diff --git a/.gitignore b/.gitignore index f7c6f512e..b5988ba55 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,6 @@ refresh_images.sh update_diffs/ create_cold_standby.sh !data/conf/nginx/mailcow_auth.conf -data/conf/postfix/postfix-tlspol \ No newline at end of file +data/conf/postfix/postfix-tlspol +tests/php/vendor/ +tests/php/.phpunit.cache/ diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index cf70e11e5..7451ed161 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -105,7 +105,6 @@ RUN addgroup -g 5000 vmail \ perl-package-stash-xs \ perl-par-packer \ perl-parse-recdescent \ - perl-lockfile-simple \ libproc2 \ perl-readonly \ perl-regexp-common \ @@ -150,7 +149,7 @@ COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY imapsync /usr/local/bin/imapsync -COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl +COPY imapsync_runner.py /usr/local/bin/imapsync_runner.py COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index e58abbb6a..d5175e96d 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -279,7 +279,7 @@ chmod g+rw /dev/console chown root:tty /dev/console chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \ /usr/lib/dovecot/sieve/rspamd-pipe-spam \ - /usr/local/bin/imapsync_runner.pl \ + /usr/local/bin/imapsync_runner.py \ /usr/local/bin/imapsync \ /usr/local/bin/trim_logs.sh \ /usr/local/bin/sa-rules.sh \ diff --git a/data/Dockerfiles/dovecot/imapsync b/data/Dockerfiles/dovecot/imapsync index de63d658c..f6beca4b0 100755 --- a/data/Dockerfiles/dovecot/imapsync +++ b/data/Dockerfiles/dovecot/imapsync @@ -1,6 +1,6 @@ #!/usr/bin/env perl -# $Id: imapsync,v 2.178 2022/01/12 21:28:37 gilles Exp gilles $ +# $Id: imapsync,v 2.314 2025/09/23 16:55:17 gilles Exp gilles $ # structure # pod documentation # use pragmas @@ -19,13 +19,12 @@ =head1 NAME -imapsync - Email IMAP tool for syncing, copying, migrating -and archiving email mailboxes between two imap servers, one way, +imapsync - Email tool for syncing two IMAP accounts, one way, and without duplicates. =head1 VERSION -This documentation refers to Imapsync $Revision: 2.178 $ +This documentation refers to Imapsync $Revision: 2.314 $ =head1 USAGE @@ -42,100 +41,120 @@ This documentation refers to Imapsync $Revision: 2.178 $ =head1 DESCRIPTION We sometimes need to transfer mailboxes from one imap server to -one another. +another. -Imapsync command is a tool allowing incremental and -recursive imap transfers from one mailbox to another. -If you don't understand the previous sentence, it's normal, -it's pedantic computer-oriented jargon. +Imapsync command is a tool allowing incremental and recursive imap +transfers from one mailbox to another. If you don't understand the +previous sentence, it's normal; it's pedantic computer-oriented +jargon. -All folders are transferred, recursively, meaning -the whole folder hierarchy is taken, all messages in them, -and all message flags (\Seen \Answered \Flagged etc.) -are synced too. +All folders are transferred, recursively, meaning the whole folder +hierarchy is taken, all sub-folders, all messages in them, and all +message flags (\Seen, \Answered, \Flagged, etc.) are synced too. -Imapsync reduces the amount of data transferred by not transferring -a given message if it already resides on the destination side. -Messages that are on the destination side but not on the -source side stay as they are. See the --delete2 -option to have strict sync and delete them. +Imapsync reduces the amount of data transferred by not transferring a +given message if it already resides on the destination side. Messages +that are on the destination side but not on the source side stay as +they are. There is no deletions by default. See the --delete2 option +to have strict sync and delete them. -How imapsync know a message is already on both sides? -Same specific headers and the transfer is done only once. -By default, the identification headers are -"Message-Id:" and "Received:" lines -but this choice can be changed with the --useheader option, -most often a duplicate problem is solved by using ---useheader "Message-Id" +How does imapsync know a message is already on both sides? Same +specific headers, and the transfer is done only once. By default, the +identification headers are "Message-Id:" and "Received:" lines. +This choice can be changed with the --useheader option. Most often, a +duplicate problem is solved by using --useheader "Message-Id" -All flags are preserved, unread messages will stay unread, -read ones will stay read, deleted will stay deleted. -In the IMAP protocol, a deleted message is not really deleted, -it is marked \Deleted and can be undelete. Real destruction -comes with the EXPUNGE or UIDEXPUNGE IMAP commands. +All flags are preserved, unread messages will stay unread, read ones +will stay read, and deleted ones will stay deleted. -You can abort the transfer at any time and restart it later, -imapsync works well with bad connections and interruptions, -by design. On a terminal hit Ctr-c twice within two seconds -to abort the program. Hit Ctr-c just once makes -imapsync reconnect to both imap servers. +Flags are also resynchronized at each run, unless the option +--noresyncflags is used. + +A point about message deletions. In the IMAP protocol, a deleted +message is not deleted, it is marked \Deleted and can be undeleted. +Real destruction comes with the EXPUNGE or UIDEXPUNGE IMAP +commands. + +You can abort the transfer at any time and restart it later. Imapsync +works well with bad connections and interruptions, by design. On a +terminal, hit Ctrl-c twice within two seconds to abort the program. Hit +Ctrl-c just once to make imapsync reconnect to both imap servers. How do you know the sync is finished and well done? -When imapsync ends by itself it mentions it with lines like those: +When imapsync ends by itself, it mentions it with lines like those: - Exiting with return value 0 (EX_OK: successful termination) 0/50 nb_errors/max_errors PID 301 - Removing pidfile /tmp/imapsync.pid - Log file is LOG_imapsync/2020_11_17_15_59_22_761_test1_test2.txt ( to change it, use --logfile filepath ; or use --nolog to turn off logging ) + Exiting with return value 0 (EX_OK: successful termination) 0/50 nb_errors/max_errors PID 20332 + Removing pidfile /home/gilles/tmp/imapsync.pid + Log file is LOG_imapsync/2023_08_21_11_51_27_651_test1_test2.txt ( to change it, use --logfile filepath ; or use --nolog to turn off logging ) -If you don't have those lines it means that either the sync process is still -running (or eventually hanging indefinitely) or that it ended without -a whisper, a strong kill -9 on Linux for example. +If you don't have those lines, it means that either the sync process is +still running (or eventually hanging indefinitely) or that it ended +without a whisper, a strong kill -9 on Linux, for example. -If you have those final lines then it means the sync process is properly -finished. It may have encountered problems though. +If you have those final lines, then it means the sync process is properly +finished. It may have encountered problems, though. -A good synchronization is mentioned by some lines above the last ones, especially -those three lines: +A good synchronization is mentioned by some lines above the last ones, +especially those three lines: The sync looks good, all 1745 identified messages in host1 are on host2. There is no unidentified message on host1. Detected 0 errors +Imapsync mentions the total sizes of both accounts at the beginning of +the sync and also at the end. Sometimes, even with a strict sync, +those total sizes differ, and sometimes they differ a lot. The +difference is not a good criterion to conclude the sync went wrong. -A classical scenario is synchronizing a mailbox B from another mailbox A -where you just want to keep a strict copy of A in B. Strict meaning -all messages in A will be in B but no more. +Why? That's because message sizes given by the imap servers are not +always accurate; they are not always the same as the actual message +sizes of the messages transferred by imapsync. Imapsync uses the sizes +given by the imap servers to calculate the total size. They can +differ. In the early days, Imapsync used the sizes of the messages as +one of the criteria to identify the messages; different sizes implied +different messages. But it was a mistake, the same message had +different sizes on both sides sometimes, depending on the imap +servers. -For this, option --delete2 can be used, it deletes messages in the host2 -folder B that are not in the host1 folder A. If you also need to destroy -host2 folders that are not in host1 then use --delete2folders. See also ---delete2foldersonly and --delete2foldersbutnot to set up exceptions -on folders to destroy. INBOX will never be destroyed, it's a mandatory -folder in IMAP so imapsync doesn't even try to remove it. +Another explanation for a big total size difference is that Gmail +doesn't count the size of duplicate messages across folders twice, +while imapsync does. + + +A classical scenario is synchronizing a mailbox B from another mailbox +A, where you just want to keep a strict copy of A in B. Strict means +that all messages in A will be in B but no more. + +For a strict synchronization, use the option --delete2. The option +--delete2 deletes the messages in the host2 folder B that are not in +the host1 folder A. If you also need to destroy host2 folders that are +not in host1, then use --delete2folders. See also --delete2foldersonly +and --delete2foldersbutnot to set up exceptions on folders to +destroy. INBOX will never be destroyed; it's a mandatory folder in +IMAP, so imapsync doesn't even try to remove it. A different scenario is to delete the messages from the source mailbox after a successful transfer, it can be a good feature when migrating mailboxes since messages will be only on one side. The source account -will only have messages that are not on the destination yet, ie, +will only have messages that are not on the destination yet, i.e., messages that arrived after a sync or that failed to be transferred. In that case, use the --delete1 option. Option --delete1 implies also -the option --expunge1 so all messages marked deleted on host1 will be -deleted. In IMAP protocol deleting a message does not delete it, -it marks it with the flag \Deleted, allowing an undelete. Expunging -a folder removes, definitively, all the messages marked as \Deleted -in this folder. +the option --expunge1, so all messages marked deleted on host1 will be +deleted. In the IMAP protocol, deleting a message does not delete it; it +marks it with the flag \Deleted, allowing an undelete. Expunging a +folder removes, definitively, all the messages marked as \Deleted in +this folder. You can also decide to remove empty folders once all of their messages have been transferred. Add --delete1emptyfolders to obtain this behavior. - -Imapsync is not adequate for maintaining two active imap accounts -in synchronization when the user plays independently on both sides. -Use offlineimap (written by John Goerzen) or mbsync (written by -Michael R. Elkins) for a 2 ways synchronization. +Imapsync is not adequate for maintaining two active imap accounts in +synchronization when the user plays independently on both sides. Use +offlineimap (written by John Goerzen) or mbsync (written by Michael +R. Elkins) for a 2 ways synchronization. =head1 OPTIONS @@ -143,32 +162,43 @@ Michael R. Elkins) for a 2 ways synchronization. usage: imapsync [options] The standard options are the six values forming the credentials. -Three values on each side are needed in order to login into the IMAP -servers. These six values are a hostname, a username, and a password, two times. +Three values on each side are needed to login to the IMAP +servers. These six values are a hostname, a username, and a password, +twice. -Conventions used in the following descriptions of the options: +Here are the conventions used in the following descriptions of the +options: - str means string - int means integer number - flo means float number - reg means regular expression - cmd means command + str means a string + int means an integer number + flo means a float number + reg means a regular expression + cmd means a command - --dry : Makes imapsync doing nothing for real; it just print what + --dry : Makes imapsync do nothing for real; it just prints what would be done without --dry. =head2 OPTIONS/credentials - --host1 str : Source or "from" imap server. + --host1 str : Source or "from" imap server. + It can be a hostname or an IP address. + --port1 int : Port to connect on host1. - Optional since default ports are the - well known ports imap/143 or imaps/993. + Optional since the default port is the + well-known port imap/143 or imaps/993. + Add --ssl1 if you want an SSL/TLS connection on host1. + --user1 str : User to login on host1. + --password1 str : Password of user1. --host2 str : "destination" imap server. - --port2 int : Port to connect on host2. Optional + --port2 int : Port to connect on host2. + Optional since the default port is the + well-known port imap/143 or imaps/993. + Add --ssl2 if you want an SSL/TLS connection on host2. + --user2 str : User to login on host2. --password2 str : Password of user2. @@ -186,31 +216,44 @@ Conventions used in the following descriptions of the options: You can also pass the passwords in the environment variables IMAPSYNC_PASSWORD1 and IMAPSYNC_PASSWORD2. If you don't pass the user1 password via --password1 nor --passfile1 nor $IMAPSYNC_PASSWORD1 -then imapsync will prompt to enter the password on the terminal. -Same thing for user2 password. +then imapsync will prompt you to enter the password on the terminal. +Same thing for the user2 password. =head2 OPTIONS/encryption - --nossl1 : Do not use a SSL connection on host1. - --ssl1 : Use a SSL connection on host1. On by default if possible. + --nossl1 : Do not use an IMAP over SSL/TLS connection on host1. - --nossl2 : Do not use a SSL connection on host2. - --ssl2 : Use a SSL connection on host2. On by default if possible. + --ssl1 : Use an IMAP over SSL/TLS connection on host1. + It is imaps. The default port is 993. + Used by default, if possible. - --notls1 : Do not use a TLS connection on host1. - --tls1 : Use a TLS connection on host1. On by default if possible. + --nossl2 : Do not use an IMAP over SSL/TLS connection on host2. - --notls2 : Do not use a TLS connection on host2. - --tls2 : Use a TLS connection on host2. On by default if possible. + --ssl2 : Use an IMAP over SSL/TLS connection on host2. + Same as --ssl1 but for host2. - --debugssl int : SSL debug mode from 0 to 4. + --notls1 : Do not try a "STARTTLS" SSL/TLS connection on host1. - --sslargs1 str : Pass any ssl parameter for host1 ssl or tls connection. Example: - --sslargs1 SSL_verify_mode=1 --sslargs1 SSL_version=SSLv3 + --tls1 : Use a "STARTTLS" SSL/TLS encryption on host1. + The default port is 143. + Used by default, after imaps probing failed, + and if possible. + + --notls2 : Do not use a "STARTTLS" SSL/TLS connection on host2. + + --tls2 : Use a STARTTLS SSL/TLS encryption on host2. + Same as --tls1 but for host2. + + --debugssl int : SSL/TLS debug mode from 0 to 4. + + --sslargs1 str : Pass any SSL/TLS parameter for the host1 connection. + Example: + --sslargs1 SSL_verify_mode=1 --sslargs1 SSL_version=TLSv1_3 See all possibilities in the new() method of IO::Socket::SSL http://search.cpan.org/perldoc?IO::Socket::SSL#Description_Of_Methods - --sslargs2 str : Pass any ssl parameter for host2 ssl or tls connection. - See --sslargs1 + + --sslargs2 str : Pass any SSL/TLS parameter for the host2 connection. + Same as --sslargs1 but for host2. =head2 OPTIONS/authentication @@ -232,27 +275,31 @@ Same thing for user2 password. --domain1 str : Domain on host1 (NTLM authentication). --domain2 str : Domain on host2 (NTLM authentication). - --oauthaccesstoken1 str : The access token to authenticate with OAUTH2. - It will be combined with the --user1 value to form the - string to pass with XOAUTH2 authentication. - The password given by --password1 or --passfile1 - is ignored. - Instead of the access token itself, the value can be a - file containing the access token on the first line. + --oauthaccesstoken1 str : The access token to authenticate with OAUTH2 on + host1. It will be combined with the --user1 value to + form the string to pass with XOAUTH2 authentication. + Instead of the access token itself, the value can be + the file containing the access token on the first line. If the value is a file, imapsync reads its first line - and take this line as the access token. The advantage + and takes this line as the access token. The advantage of the file is that if the access token changes then - imapsync can read it again when it needs to reconnect + imapsync can read it again when it needs to reconnect during a run. + --oauthaccesstoken2 str : same thing as --oauthaccesstoken1 but for host2. - --oauthaccesstoken2 str : same thing as --oauthaccesstoken1 + --oauthdirect1 str : The direct string to pass with XOAUTH2 authentication + for host1. - --oauthdirect1 str : The direct string to pass with XOAUTH2 authentication. - The password given by --password1 or --passfile1 and - the user given by --user1 are ignored. + --oauthdirect2 str : same thing as --oauthdirect1 but for host2. + + --oauthrefreshcmd1 str : An optional command to call, to refresh the host1 + token before any oauth authentication. + It can have arguments like "cmd arg1 arg2 ..." + + --oauthrefreshcmd2 str : same thing as --oauthrefreshcmd1 but for host2. + - --oauthdirect2 str : same thing as oauthdirect1 =head2 OPTIONS/folders @@ -260,7 +307,7 @@ Same thing for user2 password. --folder str : Sync this folder. --folder str : and this one, etc. - --folderrec str : Sync this folder recursively. + --folderrec str : Sync this folder and all its sub-folders. --folderrec str : and this one, etc. --folderfirst str : Sync this folder first. Ex. --folderfirst "INBOX" @@ -270,9 +317,9 @@ Same thing for user2 password. --nomixfolders : Do not merge folders when host1 is case-sensitive while host2 is not (like Exchange). Only the first - similar folder is synced (example: with folders - "Sent", "SENT" and "sent" - on host1 only "Sent" will be synced to host2). + similar folder is synced. Example: with folders + "Sent", "SENT" and "sent" on host1, only "Sent" + will be synced to host2. --skipemptyfolders : Empty host1 folders are not created on host2. @@ -285,24 +332,25 @@ Same thing for user2 password. --exclude 'fold1|fold2|f3' skips fold1, fold2 and f3. --exclude reg : or this one, etc. - --automap : guesses folders mapping, for folders well known as + --automap : guesses folder mapping, for folders well known as "Sent", "Junk", "Drafts", "All", "Archive", "Flagged". --f1f2 str1=str2 : Force folder str1 to be synced to str2, --f1f2 overrides --automap and --regextrans2. Use several --f1f2 options to map several folders. - Option --f1f2 is a one to one only folder mapping, - str1 and str2 have to be full path folder names. + Option --f1f2 is a one-to-one-only folder mapping, + and str1 and str2 must be full path folder names. + Use --regextrans2 for more general mappings. --subfolder2 str : Syncs the whole host1 folders hierarchy under the host2 folder named str. It does it internally by adding three --regextrans2 options before all others. - Add --debug to see what's really going on. + Add --debug to see what's going on. --subfolder1 str : Syncs the host1 folders hierarchy which is under folder str to the root hierarchy of host2. - It's the couterpart of a sync done by --subfolder2 + It's the counterpart of a sync done by --subfolder2 when doing it in the reverse order. Backup/Restore scenario: Use --subfolder2 str for a backup to the folder str @@ -319,7 +367,7 @@ Same thing for user2 password. --prefix1 str : Remove prefix str to all destination folders, usually "INBOX." or "INBOX/" or an empty string "". - imapsync guesses the prefix if host1 imap server + imapsync guesses the prefix if the host1 imap server does not have NAMESPACE capability. So this option should not be used most of the time. --prefix2 str : Add prefix to all host2 folders. See --prefix1 @@ -328,12 +376,12 @@ Same thing for user2 password. most of the time. Imapsync gets the separator from the server itself, by using NAMESPACE, or it tries to guess it - from the folders listing (it counts + from the folder listing (it counts characters / . \\ \ in folder names and choose the more frequent, or finally / if nothing is found. --sep2 str : Host2 separator. See --sep1 - --regextrans2 reg : Apply the whole regex to each destination folders. + --regextrans2 reg : Apply the whole regex to each destination folder. --regextrans2 reg : and this one. etc. When you play with the --regextrans2 option, first add also the safe options --dry --justfolders @@ -347,9 +395,11 @@ Same thing for user2 password. =head2 OPTIONS/folders sizes --nofoldersizes : Do not calculate the size of each folder at the - beginning of the sync. Default is to calculate them. + beginning of the sync. The default behaviour + calculates them. --nofoldersizesatend: Do not calculate the size of each folder at the - end of the sync. Default is to calculate them. + end of the sync. The default behaviour + calculates them. --justfoldersizes : Exit after having printed the initial folder sizes. @@ -358,12 +408,15 @@ Same thing for user2 password. --tmpdir str : Where to store temporary files and subdirectories. Will be created if it doesn't exist. - Default is system specific, Unix is /tmp but - /tmp is often too small and deleted at reboot. - --tmpdir /var/tmp should be better. + Default is $HOME/tmp. It used to be /tmp on Unix, + before imapsync 2.250. But /tmp is not safe, see + https://github.com/imapsync/imapsync/issues/399 + https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-34204 + - --pidfile str : The file where imapsync pid is written, - it can be dirname/filename complete path. + --pidfile str : The file where the imapsync pid is written. + It can be specified with the form dirname/filename, + ie, a complete path. The default name is imapsync.pid in tmpdir. --pidfilelocking : Abort if pidfile already exists. Useful to avoid @@ -372,19 +425,20 @@ Same thing for user2 password. =head2 OPTIONS/log - --nolog : Turn off logging on file + --nolog : Turn off logging on file. By default in Docker context. + --log : Turn on logging on file. By default in normal and CGI contextes. --logfile str : Change the default log filename (can be dirname/filename). --logdir str : Change the default log directory. Default is LOG_imapsync/ -The default logfile name is for example +The default logfile name is, for example: LOG_imapsync/2019_12_22_23_57_59_532_user1_user2.txt where: - 2019_12_22_23_57_59_532 is nearly the date of the start - YYYY_MM_DD_HH_MM_SS_mmm - year_month_day_hour_minute_seconde_millisecond + 2019_12_22_23_57_59_532 is nearly the date of the start in the format + YYYY_MM_DD_HH_MM_SS_mmm corresponding to the meaning + year_month_day_hour_minute_second_millisecond and user1 user2 are the --user1 --user2 values. @@ -421,17 +475,22 @@ and user1 user2 are the --user1 --user2 values. Example: 's/\000/ /g' # to replace null characters by spaces. --regexmess reg : and this one, etc. - - --truncmess int : truncates messages when their size exceed the int - value, specified in bytes. Good to sync too big - messages or to "suppress" attachments. - Have in mind that this way, messages become + + --truncmess int : truncates messages when their size exceeds the int + value, specified in bytes. Good to sync too big + messages or to "suppress" attachments. + Have in mind that this way, messages become uncoherent somehow. + --nodry1 : makes imapsync fetching messages from host1, + even when --dry is on. + --dry --nodry1 is useful when debugging transformation + options like --pipemess or --regexmess + =head2 OPTIONS/labels -Gmail present labels as folders in imap. Imapsync can accelerate the sync -by syncing X-GM-LABELS, it will avoid to transfer messages when they are +Gmail presents labels as folders in imap. Imapsync can accelerate the sync +by syncing X-GM-LABELS; it will avoid to transferring messages when they are already on host2 in another folder. @@ -448,7 +507,7 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt =head2 OPTIONS/flags - If you encounter flag problems see also: + If you encounter flag problems, see also: https://imapsync.lamiral.info/FAQ.d/FAQ.Flags.txt --regexflag reg : Apply the whole regex to each flags list. @@ -458,7 +517,7 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --resyncflags : Resync flags for already transferred messages. On by default. --noresyncflags : Do not resync flags for already transferred messages. - May be useful when a user has already started to play + It may be useful when a user has already started to play with its host2 account. --filterbuggyflags : Filter flags known to be buggy and generators of errors @@ -468,18 +527,18 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --delete1 : Deletes messages on host1 server after a successful transfer. Option --delete1 has the following behavior: - it marks messages as deleted with the IMAP flag - \Deleted, then messages are really deleted with an + It marks messages with the IMAP flag + \Deleted, then the messages are deleted with an EXPUNGE IMAP command. If expunging after each message - slows down too much the sync then use + slows down the sync too much, then use --noexpungeaftereach to speed up, expunging will then be done only twice per folder, one at the beginning and one at the end of a folder sync. --expunge1 : Expunge messages on host1 just before syncing a folder. Expunge is done per folder. - Expunge aims is to really delete messages marked deleted. - An expunge is also done after each message copied + Expunge aims to really delete messages marked \Deleted. + An expunge is also done after each message is copied if option --delete1 is set (unless --noexpungeaftereach). --noexpunge1 : Do not expunge messages on host1. @@ -494,11 +553,13 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --delete2duplicates : Deletes messages in host2 that are duplicates in host2. Works only without --useuid since duplicates are - detected with an header part of each message. + detected with a header part of each message. NB: --delete2duplicates is far less violent than --delete2 since it removes only duplicates. --delete2folders : Delete folders in host2 that are not in host1. + Only the selected folders are considered; all folders by + default but not all with --exclude --include --folder etc. For safety, first try it like this, it is safe: --delete2folders --dry --justfolders --nofoldersizes and see what folders will be deleted. @@ -522,9 +583,9 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt https://imapsync.lamiral.info/FAQ.d/FAQ.Dates.txt --syncinternaldates : Sets the internal dates on host2 as the same as host1. - Turned on by default. Internal date is the date - a message arrived on a host (Unix mtime usually). - --idatefromheader : Sets the internal dates on host2 as same as the + Turned on by default. The internal date is the date + when a message arrives on a host (Unix mtime usually). + --idatefromheader : Sets the internal dates on host2 to the same as the ones in "Date:" headers. @@ -535,19 +596,19 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --minsize int : Skip messages smaller (or equal) than int bytes --maxage int : Skip messages older than int days. - final stats (skipped) don't count older messages - see also --minage + The final stats (skipped) don't count the older messages. + See also --minage --minage int : Skip messages newer than int days. - final stats (skipped) don't count newer messages - You can do (+ zone are the messages selected): + The final stats (skipped) don't count newer messages + What you can do (+ zone are the messages selected): past|----maxage+++++++++++++++>now past|+++++++++++++++minage---->now past|----maxage+++++minage---->now (intersection) - past|++++minage-----maxage++++>now (union) + past|++++minage-----maxage++++>now (union, magic!) --search str : Selects only messages returned by this IMAP SEARCH command. Applied on both sides. - For a complete set of what can be search see + For a complete set of what can be searched, see https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Selection.txt --search1 str : Same as --search but for selecting host1 messages only. @@ -556,16 +617,16 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --noabletosearch : Makes --minage and --maxage options use the internal dates given by a FETCH imap command instead of the - "Date:" header. Internal date is the arrival date + "Date:" header. The internal date is the arrival date in the mailbox. --noabletosearch equals --noabletosearch1 --noabletosearch2 --noabletosearch1 : Like --noabletosearch but for host1 only. --noabletosearch2 : Like --noabletosearch but for host2 only. - --maxlinelength int : skip messages with a line length longer than int bytes. - RFC 2822 says it must be no more than 1000 bytes but - real life servers and email clients do more. + --maxlinelength int : skip messages with a line length longer than int bytes. + RFC 2822 says it must be no more than 1000 bytes, but + real-life servers and email clients do much more. --useheader str : Use this header to compare messages on both sides. @@ -585,11 +646,11 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt =head2 OPTIONS/miscellaneous - --syncacls : Synchronizes acls (Access Control Lists). - Acls in IMAP are not standardized, be careful - since one acl code on one side may signify something - else on the other one. - --nosyncacls : Does not synchronize acls. This is the default. + --syncacls : Synchronizes ACLs (Access Control Lists). + ACLs in IMAP are not standardized; be careful, + since one ACL code on one side may signify something + else on the other side. + --nosyncacls : Does not synchronize ACLs. This is the default. --addheader : When a message has no headers to be identified, --addheader adds a "Message-Id" header, @@ -600,22 +661,26 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt =head2 OPTIONS/debugging - --debug : Debug mode. - --debugfolders : Debug mode for the folders part only. - --debugcontent : Debug content of the messages transferred. Huge output. - --debugflags : Debug mode for flags. - --debugimap1 : IMAP debug mode for host1. Very verbose. - --debugimap2 : IMAP debug mode for host2. Very verbose. - --debugimap : IMAP debug mode for host1 and host2. Twice very verbose. - --debugmemory : Debug mode showing memory consumption after each copy. + --debug : Show debug notes. + --debugfolders : Show debug notes for the folders part only. + --debugcontent : Show content of the messages transferred. Huge output. + --debugflags : Show debug notes for flags. + --debugimap1 : Show IMAP notes for host1. Very verbose. + --debugimap2 : Show IMAP notes for host2. Very verbose. + --debugimap : Show IMAP notes for host1 and host2. Twice very verbose. + --debugmemory : Show the memory consumption after each copy. - --errorsmax int : Exit when int number of errors is reached. Default is 50. + --errorsmax int : Exit when int number of errors is reached. + The default is 50 errors to quit. + + --tests : Run local non-regression tests. + Exit code 0 means everything is ok. - --tests : Run local non-regression tests. Exit code 0 means all ok. --testslive : Run a live test with test1.lamiral.info imap server. - Useful to check the basics. Needs internet connection. - --testslive6 : Run a live test with ks6ipv6.lamiral.info imap server. - Useful to check the ipv6 connectivity. Needs internet. + Useful to check the basics. It needs an internet connection. + + --testslive6 : Run a live test with tests1ipv6.lamiral.info imap server. + Useful to check the IPv6 connectivity. Needs internet. =head2 OPTIONS/specific @@ -640,22 +705,22 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --timeout2 flo : Connection timeout in seconds for host2. Default is 120 and 0 means no timeout at all. - Caveat, under CGI context, you may encounter a timeout - from the webserver, killing imapsync and the imap connexions. + Caveat emptor. Under CGI context, you may encounter a timeout + from the web server, killing imapsync and the imap connections. See the document INSTALL.OnlineUI.txt and search for "Timeout" for how to deal with this issue. --keepalive1 : https://metacpan.org/pod/Mail::IMAPClient#Keepalive - Some firewalls and network gears like to timeout connections + Some firewalls and network gears like to timeout connections prematurely if the connection sits idle. This option enables SO_KEEPALIVE on the host1 socket. --keepalive1 is on by default since imapsync release 2.169 Use --nokeepalive1 to disable it. - + --keepalive2 : Same as --keepalive2 but for host2. Use --nokeepalive2 to disable it. - --maxmessagespersecond flo : limits the average number of messages + --maxmessagespersecond flo : limits the average number of messages transferred per second. --maxbytespersecond int : limits the average transfer rate per second. @@ -674,10 +739,17 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --version : Print only the software version. --noreleasecheck : Do not check for any new imapsync release. --releasecheck : Check for new imapsync release. - it's an http request to + It is an HTTP GET request to http://imapsync.lamiral.info/prj/imapsync/VERSION - --noid : Do not send/receive IMAP "ID" command to imap servers. + --emailreport1 : Put the email final report in host1 INBOX + --emailreport2 : Put the email final report in host2 INBOX + + --noemailreport1 : Do not put the email final report in host1 INBOX + --noemailreport2 : Do not put the email final report in host2 INBOX + + --noid : Do not send/receive the IMAP "ID" command + to the imap servers. --justconnect : Just connect to both servers and print useful information. Need only --host1 and --host2 options. @@ -691,10 +763,10 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt --help : print this help. - Example: to synchronize imap account "test1" on "test1.lamiral.info" - to imap account "test2" on "test2.lamiral.info" - with test1 password "secret1" - and test2 password "secret2" + Example: to synchronize the IMAP account "test1" on "test1.lamiral.info" + to the imap account "test2" on "test2.lamiral.info" + with the test1 account password being "secret1" + and the test2 account password being "secret2" imapsync \ --host1 test1.lamiral.info --user1 test1 --password1 secret1 \ @@ -710,40 +782,43 @@ https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt =head1 SECURITY -You can use --passfile1 instead of --password1 to mention the -password since it is safer. With --password1 option, on Linux, -any user on your host can see the password by using the 'ps auxwwww' -command. Using a variable (like IMAPSYNC_PASSWORD1) is also -dangerous because of the 'ps auxwwwwe' command. So, saving -the password in a well protected file (600 or rw-------) is -the best solution. +You can use --passfile1 instead of --password1 to mention the password, +since it is safer. With the --password1 option, on Linux, any user on +your host can see the password by using the 'ps auxwwww' +command. Using a variable (like IMAPSYNC_PASSWORD1) is also dangerous +because of the 'ps auxwwwwe' command. So, saving the password in a +well-protected file (600 or rw-------) is the best solution. -Imapsync activates ssl or tls encryption by default, if possible. +Imapsync activates SSL/TLS encryption by default, if possible. What detailed behavior is under this "if possible"? -Imapsync activates ssl if the well known port imaps port (993) is open -on the imap servers. If the imaps port is closed then it open a -normal (clear) connection on port 143 but it looks for TLS support -in the CAPABILITY list of the servers. If TLS is supported -then imapsync goes to encryption with STARTTLS. +Imapsync activates the SSL/TLS mechanism if the well-known port imaps +port (993) is open on the imap servers. If the imaps port is closed, +then imapsync opens a normal (clear) connection on port 143. But then it +looks for STARTTLS support in the CAPABILITY list of the servers. If +STARTTLS is supported, then imapsync goes to an SSL/TLS encryption with +the STARTTLS IMAP command. -If the automatic ssl and the tls detections fail then imapsync will -not protect against sniffing activities on the network, especially -for passwords. +If the automatic SSL/TLS detections fail then imapsync uses clear-text +communication. Clear-text communication does not protect against +sniffing activities on the network, especially passwords. -If you want to force ssl or tls just use --ssl1 --ssl2 or --tls1 --tls2 +If you want to force SSL/TLS just use --ssl1, --ssl2 or --tls1 +--tls2. Imapsync won't go to clear-text communication with any of +these options. -See also the document FAQ.Security.txt in the FAQ.d/ directory -or at https://imapsync.lamiral.info/FAQ.d/FAQ.Security.txt +See also the document FAQ.Security.txt in the FAQ.d/ directory or at +https://imapsync.lamiral.info/FAQ.d/FAQ.Security.txt =head1 EXIT STATUS -Imapsync will exit with a 0 status (return code) if everything went good. -Otherwise, it exits with a non-zero status. That's classical Unix behavior. -Here is the list of the exit code values (an integer between 0 and 255). -In Bourne Shells, this exit code value can be retrieved within the variable -value "$?" if you read it just after the imapsync call. +Imapsync will exit with a 0 status (return code) if everything goes +well. Otherwise, it exits with a non-zero status. That's classical +Unix behavior. Here is the list of the exit code values (an integer +between 0 and 255). In Bourne Shells, this exit code value can be +retrieved within the variable value "$?" if you read it just after the +imapsync call. The names reflect their meaning: @@ -772,6 +847,8 @@ egrep '^Readonly my.*\$EX' imapsync | egrep -o 'EX.*' | sed 's_^_ _' EXIT_ERR_SELECT => 117 ; EXIT_TRANSFER_EXCEEDED => 118 ; EXIT_ERR_APPEND_VIRUS => 119 ; + EXIT_ERR_FLAGS => 120 ; + EXIT_ERR_SEARCH => 121 ; EXIT_TESTS_FAILED => 254 ; # Like Test::More API EXIT_CONNECTION_FAILURE_HOST1 => 101 ; EXIT_CONNECTION_FAILURE_HOST2 => 102 ; @@ -781,10 +858,10 @@ egrep '^Readonly my.*\$EX' imapsync | egrep -o 'EX.*' | sed 's_^_ _' =head1 LICENSE AND COPYRIGHT -Imapsync is free, open, public but not always gratis software -cover by the NOLIMIT Public License, now called NLPL. -See the LICENSE file included in the distribution or just read this -simple sentence as it IS the licence text: +Imapsync is a free, open, and public but not always gratis software +tool covered by the NOLIMIT Public License, now called NLPL. See the +LICENSE file included in the distribution, or just read the following +simple sentence as it IS the license text: "No limits to do anything with this work and this license." @@ -801,12 +878,11 @@ Gilles LAMIRAL Good feedback is always welcome. Bad feedback is very often welcome. -Gilles LAMIRAL earns his living by writing, installing, -configuring and sometimes teaching free, open and often gratis -software. Imapsync used to be "always gratis" but now it is -only "often gratis" because imapsync is sold by its author, -your servitor, a good way to maintain and support free open public -software tools over decades. +Gilles LAMIRAL earns his living by writing, installing, configuring, +and sometimes teaching free, open, and often gratis software. Imapsync +used to be always gratis, but now it is only often gratis. It's +because imapsync is sold by its author, your servitor, a good way to +maintain and support free open public software tools over decades. =head1 BUGS AND LIMITATIONS @@ -818,10 +894,9 @@ See https://imapsync.lamiral.info/S/imapservers.shtml =head1 HUGE MIGRATION -If you have many mailboxes to migrate think about a little -shell program. Write a file called file.txt (for example) -containing users and passwords. -The separator used in this example is ';' +If you have many mailboxes to migrate, think about a little shell +program. Write a file called file.txt (for example) containing users +and passwords. The separator used in this example is ';' The file.txt file contains: @@ -832,21 +907,21 @@ user004_1;password004_1;user004_2;password004_2 user005_1;password005_1;user005_2;password005_2 ... -On Unix the shell program can be: +On Unix, the shell program can be: { while IFS=';' read u1 p1 u2 p2; do imapsync --host1 imap.side1.org --user1 "$u1" --password1 "$p1" \ --host2 imap.side2.org --user2 "$u2" --password2 "$p2" ... done ; } < file.txt -On Windows the batch program can be: +On Windows, the batch program can be: FOR /F "tokens=1,2,3,4 delims=; eol=#" %%G IN (file.txt) DO imapsync ^ --host1 imap.side1.org --user1 %%G --password1 %%H ^ --host2 imap.side2.org --user2 %%I --password2 %%J ... The ... have to be replaced by nothing or any imapsync option. -Welcome in shell or batch programming ! +Welcome to shell or batch programming! You will find already written scripts at https://imapsync.lamiral.info/examples/ @@ -855,36 +930,36 @@ https://imapsync.lamiral.info/examples/ Imapsync works under any Unix with Perl. - Imapsync works under most Windows (2000, XP, Vista, Seven, Eight, Ten - and all Server releases 2000, 2003, 2008 and R2, 2012 and R2, 2016) - as a standalone binary software called imapsync.exe, - usually launched from a batch file in order to avoid always typing - the options. There is also a 32bit binary called imapsync_32bit.exe + Imapsync works under most Windows (2000, XP, Vista, Seven, Eight, + Ten, Eleven, and all Server releases 2000, 2003, 2008 and R2, 2012 + and R2, 2016) as a standalone binary software called imapsync.exe, + usually launched from a batch file to avoid always typing the + options. See https://imapsync.lamiral.info/README_Windows.txt - Imapsync works under OS X as a standalone binary - software called imapsync_bin_Darwin + Imapsync works under OS X as a standalone binary software called + imapsync_bin_Darwin See + https://imapsync.lamiral.info/INSTALL.d/INSTALL.Darwin.txt - Purchase latest imapsync at + Purchase the latest imapsync at https://imapsync.lamiral.info/ - You'll receive a link to a compressed tarball called imapsync-x.xx.tgz - where x.xx is the version number. Untar the tarball where - you want (on Unix): + You'll receive a link to a compressed tarball called + imapsync-2.xxx.tgz, where 2.xxx is the version number. Untar the + tarball where you want (on Unix): - tar xzvf imapsync-x.xx.tgz + tar xzvf imapsync-2.xxx.tgz - Go into the directory imapsync-x.xx and read the INSTALL file. - As mentioned at https://imapsync.lamiral.info/#install - the INSTALL file can also be found at - https://imapsync.lamiral.info/INSTALL.d/INSTALL.ANY.txt - It is now split in several files for each system + Go into the directory imapsync-x.xx and read the INSTALL file. As + mentioned at https://imapsync.lamiral.info/#install, the INSTALL file + can also be found at + https://imapsync.lamiral.info/INSTALL.d/INSTALL.ANY.txt It is now + split into several files for each system https://imapsync.lamiral.info/INSTALL.d/ =head1 CONFIGURATION -There is no specific configuration file for imapsync, -everything is specified by the command line parameters -and the default behavior. +There is no specific configuration file for imapsync; everything is +specified by the command line parameters and the default behavior. =head1 HACKING @@ -894,62 +969,28 @@ Feel free to hack imapsync as the NOLIMIT license permits it. =head1 SIMILAR SOFTWARE - See also https://imapsync.lamiral.info/S/external.shtml - for a better up to date list. + See https://imapsync.lamiral.info/S/external.shtml -List verified on Friday July 1, 2021. - - imapsync: https://github.com/imapsync/imapsync (this is an imapsync copy, sometimes delayed, with --noreleasecheck by default since release 1.592, 2014/05/22) - imap_tools: https://web.archive.org/web/20161228145952/http://www.athensfbc.com/imap_tools/. The imap_tools code is now at https://github.com/andrewnimmo/rick-sanders-imap-tools - imaputils: https://github.com/mtsatsenko/imaputils (very old imap_tools fork) - Doveadm-Sync: https://wiki2.dovecot.org/Tools/Doveadm/Sync ( Dovecot sync tool ) - davmail: http://davmail.sourceforge.net/ - offlineimap: http://offlineimap.org/ - fdm: https://github.com/nicm/fdm - mbsync: http://isync.sourceforge.net/ - mailsync: http://mailsync.sourceforge.net/ - mailutil: https://www.washington.edu/imap/ part of the UW IMAP toolkit. (well, seems abandoned now) - imaprepl: https://bl0rg.net/software/ http://freecode.com/projects/imap-repl/ - imapcopy (Pascal): http://www.ardiehl.de/imapcopy/ - imapcopy (Java): https://code.google.com/archive/p/imapcopy/ - imapsize: http://www.broobles.com/imapsize/ - migrationtool: http://sourceforge.net/projects/migrationtool/ - imapmigrate: http://sourceforge.net/projects/cyrus-utils/ - larch: https://github.com/rgrove/larch (derived from wonko_imapsync, good at Gmail) - wonko_imapsync: http://wonko.com/article/554 (superseded by larch) - pop2imap: http://www.linux-france.org/prj/pop2imap/ (I wrote that too) - exchange-away: http://exchange-away.sourceforge.net/ - SyncBackPro: http://www.2brightsparks.com/syncback/sbpro.html - ImapSyncClient: https://github.com/ridaamirini/ImapSyncClient - MailStore: https://www.mailstore.com/en/products/mailstore-home/ - mnIMAPSync: https://github.com/manusa/mnIMAPSync - imap-upload: http://imap-upload.sourceforge.net/ (A tool for uploading a local mbox file to IMAP4 server) - imapbackup: https://github.com/rcarmo/imapbackup (A Python script for incremental backups of IMAP mailboxes) - BitRecover email-backup 99 USD, 299 USD https://www.bitrecover.com/email-backup/. - ImportExportTools: https://addons.thunderbird.net/en-us/thunderbird/addon/importexporttools/ ImportExportTools for Mozilla Thunderbird by Paolo Kaosmos. ImportExportTools does not do IMAP. - rximapmail: https://sourceforge.net/projects/rximapmail/ - CodeTwo: https://www.codetwo.com/ but CodeTwo does imap source to Office365 only. =head1 HISTORY -I initially wrote imapsync in July 2001 because an enterprise, -called BaSystemes, paid me to install a new imap server -without losing huge old mailboxes located in a far -away remote imap server, accessible by an -often broken low-bandwidth ISDN link. +I initially wrote imapsync in July 2001 because an enterprise, called +BaSystemes, paid me to install a new imap server without losing huge +old mailboxes located in a far-away remote imap server, accessible by +an often broken low-bandwidth ISDN link. -I had to verify every mailbox was well transferred, all folders, all messages, -without wasting bandwidth or creating duplicates upon resyncs. The imapsync -design was made with the beautiful rsync command in mind. +I had to verify every mailbox was well transferred, all folders, and +all messages, without wasting bandwidth or creating duplicates upon +resyncs. The imapsync design was made with the beautiful rsync command +in mind. -Imapsync started its life as a patch of the copy_folder.pl -script. The script copy_folder.pl comes from the Mail-IMAPClient-2.1.3 perl -module tarball source (more precisely in the examples/ directory of the +Imapsync originated as a patch to the copy_folder.pl script. The +script copy_folder.pl comes from the Mail-IMAPClient-2.1.3 perl module +tarball source (more precisely, in the examples/ directory of the Mail-IMAPClient tarball). -So many changes happened since then that I wonder -if it remains any lines of the original -copy_folder.pl in imapsync source code. +So many changes have happened since then that I wonder if any lines of +the original copy_folder.pl in the imapsync source code remain. =cut @@ -966,6 +1007,7 @@ use Compress::Zlib ; use Data::Dumper ; use Digest::HMAC_SHA1 qw( hmac_sha1 hmac_sha1_hex ) ; use Digest::MD5 qw( md5 md5_hex md5_base64 ) ; +use Digest::SHA ; use Encode ; use Encode::IMAPUTF7 ; use English qw( -no_match_vars ) ; @@ -978,9 +1020,11 @@ use File::Path qw( mkpath rmtree ) ; use File::Spec ; use File::stat ; use Getopt::Long ( ) ; +# This allows to notice the unknown options given +Getopt::Long::Configure("pass_through") ; use IO::File ; use IO::Socket qw( :crlf SOL_SOCKET SO_KEEPALIVE ) ; -use IO::Socket::INET6 ; +#use IO::Socket::INET6 ; use IO::Socket::SSL ; use IO::Tee ; use IPC::Open3 'open3' ; @@ -1051,6 +1095,7 @@ Readonly my $EXIT_TRANSFER_EXCEEDED => 118 ; Readonly my $EXIT_ERR_APPEND_VIRUS => 119 ; Readonly my $EXIT_ERR_FLAGS => 120 ; +Readonly my $EXIT_ERR_SEARCH => 121 ; Readonly my $EXIT_TESTS_FAILED => 254 ; # Like Test::More API @@ -1082,6 +1127,7 @@ Readonly my %EXIT_TXT => ( $EXIT_ERR_APPEND_VIRUS => 'EXIT_ERR_APPEND_VIRUS', $EXIT_ERR_FETCH => 'EXIT_ERR_FETCH', $EXIT_ERR_FLAGS => 'EXIT_ERR_FLAGS', + $EXIT_ERR_SEARCH => 'EXIT_ERR_SEARCH', $EXIT_ERR_CREATE => 'EXIT_ERR_CREATE', $EXIT_ERR_SELECT => 'EXIT_ERR_SELECT', $EXIT_TESTS_FAILED => 'EXIT_TESTS_FAILED', @@ -1102,6 +1148,7 @@ Readonly my %EXIT_VALUE_OF_ERR_TYPE => ( ERR_SELECT => $EXIT_ERR_SELECT, ERR_Host1_FETCH => $EXIT_ERR_FETCH, ERR_FLAGS => $EXIT_ERR_FLAGS, + ERR_SEARCH => $EXIT_ERR_SEARCH, ERR_UNCLASSIFIED => $EXIT_WITH_ERRORS, ERR_NOTHING_REPORTED => $EXIT_WITH_ERRORS, ERR_TRANSFER_EXCEEDED => $EXIT_TRANSFER_EXCEEDED, @@ -1123,6 +1170,7 @@ Readonly my %COMMENT_OF_ERR_TYPE => ( ERR_SELECT => \&comment_err_blank, ERR_Host1_FETCH => \&comment_err_blank, ERR_FLAGS => \&comment_err_flags, + ERR_SEARCH => \&comment_err_search, ERR_UNCLASSIFIED => \&comment_err_blank, ERR_NOTHING_REPORTED => \&comment_err_blank, ERR_TRANSFER_EXCEEDED => \&comment_err_transfer_exceeded, @@ -1140,11 +1188,21 @@ sub comment_err_blank } + +sub comment_err_search +{ + my $mysync = shift @ARG ; + + my $comment = "Check your --search option(s), usually a syntax issue. See https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Selection.txt" ; + return $comment ; +} + + sub comment_err_append_size { my $mysync = shift @ARG ; - - my $comment = "The destination server refuses too big messages. Use --truncmess option. Read https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Too_Big.txt" ; + + my $comment = "The destination server refuses too big messages. Use --truncmess option. See https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Too_Big.txt" ; return $comment ; } @@ -1152,7 +1210,7 @@ sub comment_err_append_size sub comment_err_authentication_failure_host1 { my $mysync = shift @ARG ; - + my $comment = "Check the credentials for $mysync->{ user1 }." ; return $comment ; } @@ -1160,7 +1218,7 @@ sub comment_err_authentication_failure_host1 sub comment_err_authentication_failure_host2 { my $mysync = shift @ARG ; - + my $comment = "Check the credentials for $mysync->{ user2 }." ; return $comment ; } @@ -1169,7 +1227,7 @@ sub comment_err_authentication_failure_host2 sub comment_err_connection_failure_host1 { my $mysync = shift @ARG ; - + my $comment = "Check that host1 $mysync->{ host1 } on port $mysync->{ port1 } is the right IMAP server to be contacted for your mailbox." ; return $comment ; } @@ -1177,7 +1235,7 @@ sub comment_err_connection_failure_host1 sub comment_err_connection_failure_host2 { my $mysync = shift @ARG ; - + my $comment = "Check that host1 $mysync->{ host2 } on port $mysync->{ port2 } is the right IMAP server to be contacted for your mailbox." ; return $comment ; } @@ -1185,7 +1243,7 @@ sub comment_err_connection_failure_host2 sub comment_err_overquota { my $mysync = shift @ARG ; - + my $comment = 'The destination mailbox is 100% full, get free space on it and then resume the sync.' ; return $comment ; } @@ -1194,7 +1252,7 @@ sub comment_err_overquota sub comment_err_transfer_exceeded { my $mysync = shift @ARG ; - + my $size_limit_human = bytes_display_string_dec( $mysync->{ exitwhenover } ) ; my $comment = "The maximum transfer size for a single sync is reached ( over $size_limit_human ). Relaunch the sync to sync more." ; return $comment ; @@ -1203,7 +1261,7 @@ sub comment_err_transfer_exceeded sub comment_err_flags { my $mysync = shift @ARG ; - + my $comment = 'Many STORE errors with FLAGS. Retry with the option --noresyncflags' ; return $comment ; } @@ -1212,10 +1270,8 @@ sub comment_err_flags Readonly my $DEFAULT_LOGDIR => 'LOG_imapsync' ; -Readonly my $ERRORS_MAX => 50 ; # exit after 50 errors. -Readonly my $ERRORS_MAX_CGI => 20 ; # exit after 20 errors in CGI context. - - +Readonly my $ERRORS_MAX => 50 ; # exit after 50 errors. +Readonly my $ERRORS_MAX_CGI => 500 ; # exit after 500 errors in CGI context. Readonly my $INTERVAL_TO_EXIT => 2 ; # interval max to exit instead of reconnect @@ -1294,9 +1350,8 @@ Readonly my $FORCE => 1 ; my( $sync, $acc1, $acc2, - $debugflags, - $debuglist, $debugdev, $debugmaxlinelength, $debugcgi, - @include, @exclude, @folderrec, + $debugmaxlinelength, + @include, @exclude, @folderfirst, @folderlast, @h1_folders_all, %h1_folders_all, @h2_folders_all, %h2_folders_all, @@ -1307,19 +1362,10 @@ my( %h1_subscribed_folder, %h2_subscribed_folder, %h2_folders_from_1_wanted, %h2_folders_from_1_several, - $prefix1, $prefix2, - @regexmess, @skipmess, @pipemess, $pipemesscheck, - $syncflagsaftercopy, - $syncinternaldates, - $idatefromheader, - $minsize, $maxage, $minage, - $search, - @useheader, %useheader, + @regexmess, @skipmess, @pipemess, $skipsize, $allowsizemismatch, $buffersize, $authmd5, $authmd51, $authmd52, $subscribed, $subscribe, $subscribeall, - $help, - $nb_msg_skipped_dry_mode, $h2_nb_msg_noheader, $h1_bytes_processed, $h1_nb_msg_end, $h1_bytes_end, @@ -1327,31 +1373,20 @@ my( $timestart_int, $uid1, $uid2, $split1, $split2, - $modulesversion, - $delete2folders, $delete2foldersonly, $delete2foldersbutnot, - $usecache, $debugcache, $cacheaftercopy, - $wholeheaderifneeded, %h1_msgs_copy_by_uid, $useuid, $h2_uidguess, - $checkmessageexists, + $wholeheaderifneeded, %h1_msgs_copy_by_uid, $h2_uidguess, $messageidnodomain, - $fixInboxINBOX, - $maxlinelength, $maxlinelengthcmd, - $minmaxlinelength, - $fixcolonbug, - $create_folder_old, - $skipcrossduplicates, $debugcrossduplicates, - $disarmreadreceipts, + $maxlinelength, $mixfolders, - $fetch_hash_set, $cgidir, %month_abrev, $SSL_VERIFY_POLICY, -) ; +) ; -single_sync( $sync, $acc1, $acc2 ); +exit( single_sync( $sync, $acc1, $acc2 ) ) ; -sub single_sync +sub single_sync { # main program @@ -1363,7 +1398,8 @@ sub single_sync # or $acc1->{variable_name} # or $acc1->{variable_name} -# +# Yes, the indentation sucks, among other things. + $acc1 = {} ; $acc2 = {} ; $sync->{ acc1 } = $acc1 ; @@ -1374,30 +1410,19 @@ $acc2->{ Side } = 'Host2' ; $acc1->{ N } = '1' ; $acc2->{ N } = '2' ; -$sync->{timestart} = time ; # Is a float because of use Time::HiRres +$sync->{ timestart } = time ; # Is a float because of use Time::HiRres -$sync->{rcs} = q{$Id: imapsync,v 2.178 2022/01/12 21:28:37 gilles Exp gilles $} ; - -$sync->{ memory_consumption_at_start } = memory_consumption( ) || 0 ; +$sync->{ rcs } = q{$Id: imapsync,v 2.314 2025/09/23 16:55:17 gilles Exp gilles $} ; +$sync->{ memory_consumption_at_start } = memory_consumption_of_myself( ) || 0 ; my @loadavg = loadavg( ) ; -$sync->{ cpu_number } = cpu_number( ) ; -$sync->{ loaddelay } = load_and_delay( $sync->{ cpu_number }, @loadavg ) ; -$sync->{ loaddelay } = 0 ; - -$sync->{ loadavg } = join( q{ }, $loadavg[ 0 ] ) - . " on $sync->{cpu_number} cores and " - . ram_memory_info( ) ; - - - $sync->{ total_bytes_transferred } = 0 ; $sync->{ total_bytes_skipped } = 0 ; $sync->{ nb_msg_transferred } = 0 ; -$sync->{ nb_msg_skipped } = $nb_msg_skipped_dry_mode = 0 ; +$sync->{ nb_msg_skipped } = $sync->{ nb_msg_skipped_dry_mode } = 0 ; $sync->{ acc1 }->{ nb_msg_deleted } = 0 ; $sync->{ acc2 }->{ nb_msg_deleted } = 0 ; @@ -1417,8 +1442,9 @@ $sync->{ h1_nb_msg_processed } = $h1_bytes_processed = 0 ; $sync->{ h2_nb_msg_crossdup } = 0 ; -#$h1_nb_msg_end = $h1_bytes_end = 0 ; -#$h2_nb_msg_end = $h2_bytes_end = 0 ; + +$sync->{ deleted_folders_host2 } = 0 ; +$sync->{ deleted_folders_host2_failures } = 0 ; $sync->{ nb_errors } = 0; $sync->{ biggest_message_transferred } = 0; @@ -1439,6 +1465,14 @@ $sync->{ biggest_message_transferred } = 0; ); +my %SSL_VERIFY_STR ; + +Readonly $SSL_VERIFY_POLICY => IO::Socket::SSL::SSL_VERIFY_NONE( ) ; +Readonly %SSL_VERIFY_STR => ( + IO::Socket::SSL::SSL_VERIFY_NONE( ) => 'SSL_VERIFY_NONE, ie, do not check the server certificate.' , + IO::Socket::SSL::SSL_VERIFY_PEER( ) => 'SSL_VERIFY_PEER, ie, check the server certificate.' , +) ; + # Just create a CGI object if under cgi context only. # Needed for the get_options() call @@ -1446,54 +1480,86 @@ cgibegin( $sync ) ; # In cgi context, printing must start by the header so we delay other prints by using output() storage my $options_good = get_options( $sync, @ARGV ) ; + + +# No exit on heavy load by default in normal context. +# But exit on heavy load in CGI context +setexitonload( $sync ) ; + +$sync->{ cpu_number } = cpu_number( ) ; +$sync->{ heavy_load_reached } = heavy_load_reached( $sync ) ; +#$sync->{ heavy_load_reached} = 0 ; # uncomment this line if you want to go on no matter the load. +$sync->{ diskspace_available } = diskspace_available( '/var/tmp/imapsync_test_diskspace_available' ) ; + +$sync->{ loadavg } = join( q{ }, $loadavg[ 0 ] ) + . " on $sync->{cpu_number} cores and " + . ram_memory_info( $sync ) ; + # Is it the first myprint? cgibuildheader( $sync ) ; docker_context( $sync ) ; print_output_if_needed( $sync ) ; - - output_reset_with( $sync ) ; -# don't go on if options are not all known. + +# Do not go on if options are not all known. if ( ! defined $options_good ) { exit $EX_USAGE ; } +# Do not go on if no disk space available in CGI context. +cgi_exit_on_no_disk_space( $sync ) ; + # If you want releasecheck not to be done by default (like the github maintainer), # then just uncomment the first "$sync->{releasecheck} =" line, the line ending with "0 ;", -# the second line (ending with "1 ;") can then stay active or be commented, -# the result will be the same: no releasecheck by default (because 0 is then the defined value). +# the second line (ending with "1 ;") can stay active or be commented, +# the result will be the same: no releasecheck by default (because 0 is then a defined value). -#$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 0 ; -$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 1 ; +$sync->{ releasecheck } = defined $sync->{ releasecheck } ? $sync->{ releasecheck } : 0 ; +#$sync->{ releasecheck } = defined $sync->{ releasecheck } ? $sync->{ releasecheck } : 1 ; + + +#$sync->{debugenv} = 1 ; +$sync->{ debugenv } and printenv( $sync ) ; # if option --debugenv +load_modules( ) ; # just the version -if ( $sync->{ version } ) { +if ( $sync->{ version } ) +{ myprint( imapsync_version( $sync ), "\n" ) ; return 0 ; } -#$sync->{debugenv} = 1 ; -$sync->{debugenv} and printenv( $sync ) ; # if option --debugenv -load_modules( ) ; - # after_get_options call usage and exit if --help or options were not well got after_get_options( $sync, $options_good ) ; #local $ENV{TZ} = 'GMT' if ( under_cgi_context( $sync ) and 'MSWin32' ne $OSNAME ) ; #output( $sync, localtime(time) . " " . gmtime(time) . "\n" ) ; +make_var_array_to_a_hash( $sync ) ; + # Under CGI environment, fix caveat emptor potential issues cgisetcontext( $sync ) ; -get_options_extra( $sync ) ; +get_options_extra_from_file( $sync ) ; +get_options_extra_from_param( $sync ) ; + +if ( under_cgi_context( $sync ) ) { output( $sync, cgienvcontext( $sync ) ) } ; # --gmail --gmail --exchange --office etc. easyany( $sync ) ; + $sync->{ sanitize } = defined $sync->{ sanitize } ? $sync->{ sanitize } : 1 ; sanitize( $sync ) ; -$sync->{ tmpdir } ||= File::Spec->tmpdir( ) ; +$sync->{ tmpdir } = tmpdir( $sync ) ; + +output( $sync, 'Current directory is ' . getcwd( ) . "\n" ) ; +output( $sync, "Temp directory is $sync->{ tmpdir } ( to change it use --tmpdir dirpath )\n" ) ; +output( $sync, "Creating temp directory $sync->{ tmpdir }\n" ) ; + +eval { mkpath( $sync->{ tmpdir } ) } && chmod( 0755, $sync->{ tmpdir } ) ; + # Unit tests my $unittestssuite = unittestssuite( $sync ) ; @@ -1501,7 +1567,7 @@ my $unittestssuite = unittestssuite( $sync ) ; if ( condition_to_leave_after_tests( $sync ) ) { - return $unittestssuite ; + return( $unittestssuite ) ; } # init live varaiables @@ -1511,6 +1577,11 @@ if ( $sync->{ testslive } ) testslive_init( $sync ) ; } +if ( $sync->{ testslive2 } ) +{ + testslive2_init( $sync ) ; +} + if ( $sync->{ testslive6 } ) { testslive6_init( $sync ) ; @@ -1521,32 +1592,53 @@ if ( $sync->{ abortbyfile } ) { $sync->{ abort } = 1 ; } install_signals( $sync ) ; -$sync->{ log } = defined $sync->{ log } ? $sync->{ log } : 1 ; +$sync->{ loglogfilename } = '../list_all_logs_auto.txt' ; + + +# $sync->{ log } = defined $sync->{ log } ? $sync->{ log } : 1 ; + +setlog( $sync ) ; + $sync->{ errorsdump } = defined $sync->{ errorsdump } ? $sync->{ errorsdump } : 1 ; $sync->{ errorsmax } = defined $sync->{ errorsmax } ? $sync->{ errorsmax } : $ERRORS_MAX ; +$sync->{ emailreport1 } = defined $sync->{ emailreport1 } ? $sync->{ emailreport1 } : 0 ; +$sync->{ emailreport2 } = defined $sync->{ emailreport2 } ? $sync->{ emailreport2 } : 0 ; + + # log and output binmode STDOUT, ":encoding(UTF-8)" ; -if ( $sync->{ log } ) { - setlogfile( $sync ) ; - teelaunch( $sync ) ; +if ( $sync->{ log } ) +{ + $sync->{ logdir } = setlogdir( $sync ) ; + $sync->{ logfile } = setlogfile( $sync, $sync->{ logfile } ) ; + $sync->{ tee } = teelaunch( $sync, $sync->{ logfile } ) ; # now $sync->{tee} is a filehandle to STDOUT and the logfile -} +} #binmode STDERR, ":encoding(UTF-8)" ; -# STDERR goes to the same place: LOG and STDOUT (if logging is on) -# Useful only for --debugssl -$sync->{tee} and local *STDERR = *${$sync->{tee}}{IO} ; +# STDERR goes to the same place: LOG and STDOUT if logging is on +# or just STDOUT +# + +stderr_to_stdout( $sync ) ; + + +if ( usecache_and_skipcrossduplicates( $sync ) ) +{ + $sync->{ nb_errors }++ ; + exit_clean( $sync, $EX_USAGE, "Error: can not have both --usecache and --skipcrossduplicates\n" ) ; +} -$timestart_int = int( $sync->{timestart} ) ; -$sync->{timebefore} = $sync->{timestart} ; +$timestart_int = int( $sync->{ timestart } ) ; +$sync->{timebefore} = $sync->{ timestart } ; -$sync->{ timestart_str } = localtimez( $sync->{timestart} ) ; +$sync->{ timestart_str } = localtimez( $sync->{ timestart } ) ; # The prints in the log starts here @@ -1555,12 +1647,12 @@ myprint( "Transfer started at $sync->{ timestart_str }\n" ) ; myprint( "PID is $PROCESS_ID my PPID is ", mygetppid( ), "\n" ) ; announcelogfile( $sync ) ; myprint( "Load is " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $sync->{cpu_number} cores\n" ) ; -#myprintf( "Memory consumption so far: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; -myprint( 'Current directory is ' . getcwd( ) . "\n" ) ; +#myprintf( "Memory consumption so far: %.1f MiB\n", memory_consumption_of_myself( ) / $KIBI / $KIBI ) ; +#myprint( 'Current directory is ' . getcwd( ) . "\n" ) ; myprint( 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; myprint( 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; -$modulesversion = defined $modulesversion ? $modulesversion : 1 ; +$sync->{ modulesversion } = defined $sync->{ modulesversion } ? $sync->{ modulesversion } : 1 ; $sync->{ warn_release } = ( $sync->{ releasecheck } ) ? check_last_release( ) : $STR_use_releasecheck ; @@ -1568,32 +1660,30 @@ $sync->{ warn_release } = ( $sync->{ releasecheck } ) ? check_last_release( ) : $wholeheaderifneeded = defined $wholeheaderifneeded ? $wholeheaderifneeded : 1; # Activate --usecache if --useuid is set and there is no --nousecache -$usecache = 1 if ( $useuid and ( ! defined $usecache ) ) ; -$cacheaftercopy = 1 if ( $usecache and ( ! defined $cacheaftercopy ) ) ; - - - +$sync->{ usecache } = 1 if ( $sync->{ useuid } and ( ! defined $sync->{ usecache } ) ) ; +$sync->{ cacheaftercopy } = 1 if ( $sync->{ usecache } and ( ! defined $sync->{ cacheaftercopy } ) ) ; $sync->{ checkfoldersexist } = defined $sync->{ checkfoldersexist } ? $sync->{ checkfoldersexist } : 1 ; -$checkmessageexists = defined $checkmessageexists ? $checkmessageexists : 0 ; +$sync->{ checkmessageexists } = defined $sync->{ checkmessageexists } ? $sync->{ checkmessageexists } : 0 ; $sync->{ expungeaftereach } = defined $sync->{ expungeaftereach } ? $sync->{ expungeaftereach } : 1 ; # abletosearch is on by default -$sync->{abletosearch} = defined $sync->{abletosearch} ? $sync->{abletosearch} : 1 ; -$sync->{abletosearch1} = defined $sync->{abletosearch1} ? $sync->{abletosearch1} : $sync->{abletosearch} ; -$sync->{abletosearch2} = defined $sync->{abletosearch2} ? $sync->{abletosearch2} : $sync->{abletosearch} ; -$checkmessageexists = 0 if ( not $sync->{abletosearch1} ) ; +$sync->{ abletosearch } = defined $sync->{ abletosearch } ? $sync->{ abletosearch } : 1 ; +$sync->{ abletosearch1 } = defined $sync->{ abletosearch1 } ? $sync->{ abletosearch1 } : $sync->{ abletosearch } ; +$sync->{ abletosearch2 } = defined $sync->{ abletosearch2 } ? $sync->{ abletosearch2 } : $sync->{ abletosearch } ; +$sync->{ checkmessageexists } = 0 if ( not $sync->{ abletosearch1 } ) ; -$sync->{ trylogin } = defined $sync->{ trylogin } ? $sync->{ trylogin } : 1 ; -$sync->{showpasswords} = defined $sync->{showpasswords} ? $sync->{showpasswords} : 0 ; -$sync->{ fixslash2 } = defined $sync->{ fixslash2 } ? $sync->{ fixslash2 } : 1 ; -$fixInboxINBOX = defined $fixInboxINBOX ? $fixInboxINBOX : 1 ; -$create_folder_old = defined $create_folder_old ? $create_folder_old : 0 ; -$mixfolders = defined $mixfolders ? $mixfolders : 1 ; -$sync->{automap} = defined $sync->{automap} ? $sync->{automap} : 0 ; +$sync->{ trylogin } = defined $sync->{ trylogin } ? $sync->{ trylogin } : 1 ; +$sync->{ showpasswords } = defined $sync->{ showpasswords } ? $sync->{showpasswords} : 0 ; +$sync->{ fixslash2 } = defined $sync->{ fixslash2 } ? $sync->{ fixslash2 } : 1 ; +$sync->{ fixInboxINBOX } = defined $sync->{ fixInboxINBOX } ? $sync->{ fixInboxINBOX } : 1 ; +$sync->{ create_folder_old } = defined $sync->{ create_folder_old } ? $sync->{ create_folder_old } : 0 ; +$mixfolders = defined $mixfolders ? $mixfolders : 1 ; +$sync->{automap} = defined $sync->{automap} ? $sync->{automap} : 0 ; + +$sync->{ delete2duplicates } = determine_delete2duplicates( $sync ) ; -$sync->{ delete2duplicates } = 1 if ( $sync->{ delete2 } and ( ! defined $sync->{ delete2duplicates } ) ) ; $sync->{maxmessagespersecond} = defined $sync->{maxmessagespersecond} ? $sync->{maxmessagespersecond} : 0 ; $sync->{maxbytespersecond} = defined $sync->{maxbytespersecond} ? $sync->{maxbytespersecond} : 0 ; @@ -1602,12 +1692,11 @@ $sync->{sslcheck} = defined $sync->{sslcheck} ? $sync->{sslcheck} : 1 ; myprint( banner_imapsync( $sync, @ARGV ) ) ; -myprint( "Temp directory is $sync->{ tmpdir } ( to change it use --tmpdir dirpath )\n" ) ; myprint( output( $sync ) ) ; output_reset_with( $sync ) ; -do_valid_directory( $sync->{ tmpdir } ) || croak "Error creating tmpdir $sync->{ tmpdir } : $OS_ERROR" ; +make_valid_directory( $sync, $sync->{ tmpdir } ) || croak "Error creating tmpdir $sync->{ tmpdir } : $OS_ERROR" ; remove_pidfile_not_running( $sync->{ pidfile } ) ; @@ -1619,7 +1708,8 @@ if ( $sync->{ tail } and tail( $sync ) ) exit $EX_OK ; } -if ( ! write_pidfile( $sync ) ) { +if ( ! write_pidfile( $sync ) ) +{ myprint( "Exiting with return value $EXIT_PID_FILE_ERROR ($EXIT_TXT{$EXIT_PID_FILE_ERROR}) $sync->{nb_errors}/$sync->{errorsmax} nb_errors/max_errors PID $PROCESS_ID\n" ) ; exit $EXIT_PID_FILE_ERROR ; } @@ -1637,23 +1727,26 @@ if ( $sync->{ abort } ) exit $EX_OK ; } -# simulong is just a loop printing some lines for xx seconds with option "--simulong xx". +if ( $sync->{ memorystress } ) { tests_memory_stress() ; } + + +# simulong is just a loop printing some fake ETA lines for xx seconds with option "--simulong xx". simulong( $sync ) ; +debugsleep( $sync ) ; - -# New place for cgiload 2019_03_03 +# New place for cgi_exit_on_heavy_load 2019_03_03 # because I want to log it # Can break here if load is too heavy -# Have in mind the CGI header has already a 503 Service Unavailable -cgiload( $sync ) ; +# Have in mind the CGI header has already a 503 Service Unavailable printed. +cgi_exit_on_heavy_load( $sync ) ; -$fixcolonbug = defined $fixcolonbug ? $fixcolonbug : 1 ; +$sync->{ fixcolonbug } = defined $sync->{ fixcolonbug } ? $sync->{ fixcolonbug } : 1 ; -if ( $usecache and $fixcolonbug ) { tmpdir_fix_colon_bug( $sync ) } ; +if ( $sync->{ usecache } and $sync->{ fixcolonbug } ) { tmpdir_fix_colon_bug( $sync ) } ; -$modulesversion and myprint( "Modules version list ( use --no-modulesversion to turn off printing this Perl modules list ):\n", modulesversion(), "\n" ) ; +$sync->{ modulesversion } and myprint( "Modules version list ( use --no-modulesversion to turn off printing this Perl modules list ):\n", modulesversion(), "\n" ) ; check_lib_version( $sync ) or @@ -1661,7 +1754,7 @@ check_lib_version( $sync ) or -if ( $sync->{ justbanner } ) +if ( $sync->{ justbanner } ) { myprint( "Exiting because of --justbanner\n" ) ; exit_clean( $sync, $EX_OK ) ; @@ -1679,31 +1772,32 @@ filterbuggyflags( $sync ) ; # sync flags just after an APPEND, some servers ignore the flags given in the APPEND # like MailEnable IMAP server. # Off by default since it takes time. -$syncflagsaftercopy = defined $syncflagsaftercopy ? $syncflagsaftercopy : 0 ; +$sync->{ syncflagsaftercopy } = defined $sync->{ syncflagsaftercopy } ? $sync->{ syncflagsaftercopy } : 0 ; # update flags on host2 for already transferred messages $sync->{resyncflags} = defined $sync->{resyncflags} ? $sync->{resyncflags} : 1 ; -if ( $sync->{resyncflags} ) { +if ( $sync->{resyncflags} ) +{ myprint( "Info: will resync flags for already transferred messages. Use --noresyncflags to not resync flags.\n" ) ; }else{ myprint( "Info: will not resync flags for already transferred messages. Use --resyncflags to resync flags.\n" ) ; } - +# Must call host1_port1_folderrec_from_host1() before sslcheck(). +host1_port1_folderrec_from_host1( $sync ) ; +host2_port2_subfolder2_from_host2( $sync ) ; sslcheck( $sync ) ; -#print Data::Dumper->Dump( [ \$sync ] ) ; -$split1 ||= $SPLIT ; -$split2 ||= $SPLIT ; -#$sync->{host1} || missing_option( $sync, '--host1' ) ; $sync->{host1} = sanitize_host( $sync->{host1} ) ; $sync->{port1} ||= ( $sync->{ssl1} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; -#$sync->{host2} || missing_option( $sync, '--host2' ) ; $sync->{host2} = sanitize_host( $sync->{host2} ) ; $sync->{port2} ||= ( $sync->{ssl2} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; +$split1 ||= $SPLIT ; +$split2 ||= $SPLIT ; + $acc1->{ debugimap } = $acc2->{ debugimap } = 1 if ( $sync->{ debugimap } ) ; # Set on debug mode if one of the imap dialogs are in debug. @@ -1722,91 +1816,98 @@ $subscribe = defined $subscribe ? $subscribe : 1; $allowsizemismatch = defined $allowsizemismatch ? $allowsizemismatch : 1; -if ( defined $delete2foldersbutnot or defined $delete2foldersonly ) { - $delete2folders = 1 ; +if ( defined( $sync->{ delete2foldersbutnot } ) or ( defined $sync->{ delete2foldersonly } ) ) +{ + $sync->{ delete2folders } = 1 ; } -my %SSL_VERIFY_STR ; - -Readonly $SSL_VERIFY_POLICY => IO::Socket::SSL::SSL_VERIFY_NONE( ) ; -Readonly %SSL_VERIFY_STR => ( - IO::Socket::SSL::SSL_VERIFY_NONE( ) => 'SSL_VERIFY_NONE, ie, do not check the certificate server.' , - IO::Socket::SSL::SSL_VERIFY_PEER( ) => 'SSL_VERIFY_PEER, ie, check the certificate server' , -) ; - -$IO::Socket::SSL::DEBUG = defined( $sync->{debugssl} ) ? $sync->{debugssl} : 1 ; +$IO::Socket::SSL::DEBUG = defined( $sync->{ debugssl } ) ? $sync->{ debugssl } : 1 ; -if ( $sync->{ssl1} or $sync->{ssl2} or $sync->{tls1} or $sync->{tls2}) { +if ( $sync->{ ssl1 } or $sync->{ ssl2 } or $sync->{ tls1 } or $sync->{ tls2 } ) +{ myprint( "SSL debug mode level is --debugssl $IO::Socket::SSL::DEBUG (can be set from 0 meaning no debug to 4 meaning max debug)\n" ) ; } -if ( $sync->{ssl1} ) { +if ( $sync->{ ssl1 } ) +{ myprint( qq{Host1: SSL default mode is like --sslargs1 "SSL_verify_mode=$SSL_VERIFY_POLICY", meaning for host1 $SSL_VERIFY_STR{$SSL_VERIFY_POLICY}\n} ) ; myprint( 'Host1: Use --sslargs1 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER( ) . " to have $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER( )} of host1\n" ) ; # $sync->{ acc1 }->{sslargs}->{SSL_verify_mode} } -if ( $sync->{ssl2} ) { +if ( $sync->{ ssl2 } ) +{ myprint( qq{Host2: SSL default mode is like --sslargs2 "SSL_verify_mode=$SSL_VERIFY_POLICY", meaning for host2 $SSL_VERIFY_STR{$SSL_VERIFY_POLICY}\n} ) ; myprint( 'Host2: Use --sslargs2 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER( ) . " to have $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER( )} of host2\n" ) ; } # ID on by default since 1.832 -$sync->{id} = defined $sync->{id} ? $sync->{id} : 1 ; +$sync->{ id } = defined $sync->{ id } ? $sync->{ id } : 1 ; -if ( $sync->{justconnect} - or not $sync->{user1} - or not $sync->{user2} - or not $sync->{host1} - or not $sync->{host2} - ) +my $justconnect1 = '' ; +my $justconnect2 = '' ; + +if ( trigger_justconnect1( $sync ) ) { - my $justconnect = justconnect( $sync ) ; + $justconnect1 = justconnect1( $sync ) ; +} - myprint( debugmemory( $sync, " after justconnect() call" ) ) ; +if ( trigger_justconnect2( $sync ) ) +{ + $justconnect2 = justconnect2( $sync ) ; +} + + +if ( $justconnect1 and $justconnect2 ) +{ exit_clean( $sync, $EX_OK, - "Exiting after a justconnect on host(s): $justconnect\n" + "Exiting after a justconnect on host(s): $justconnect1 $justconnect2\n" ) ; } -#$sync->{user1} || missing_option( $sync, '--user1' ) ; -#$sync->{user2} || missing_option( $sync, '--user2' ) ; - -$syncinternaldates = defined $syncinternaldates ? $syncinternaldates : 1; +$sync->{ syncinternaldates } = defined $sync->{ syncinternaldates } ? $sync->{ syncinternaldates } : 1 ; # Turn on expunge if there is not explicit option --noexpunge1 and option # --delete1 is given. # Done because --delete1 --noexpunge1 is very dangerous on the second run: # the Deleted flag is then synced to all previously transferred messages. # So --delete1 implies --expunge1 is a better usability default behavior. -if ( $sync->{ delete1 } ) { - if ( ! defined $sync->{ expunge1 } ) { +if ( $sync->{ delete1 } ) +{ + if ( ! defined $sync->{ expunge1 } ) + { myprint( "Info: turning on --expunge1 because --delete1 --noexpunge1 is very dangerous on the second run.\n" ) ; $sync->{ expunge1 } = 1 ; } - myprint( "Info: if expunging after each message slows down too much the sync then use --noexpungeaftereach to speed up\n" ) ; + myprint( "Info: if expunging after each message slows down too much the sync then use --noexpungeaftereach to speed up\n" ) ; } -if ( $sync->{ uidexpunge2 } and not Mail::IMAPClient->can( 'uidexpunge' ) ) { +if ( $sync->{ uidexpunge2 } and not Mail::IMAPClient->can( 'uidexpunge' ) ) +{ myprint( "Failure: uidexpunge not supported (IMAPClient release < 3.17), use nothing or --expunge2 instead\n" ) ; - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EX_SOFTWARE ) ; } -if ( ( $sync->{ delete2 } or $sync->{ delete2duplicates } ) and not defined $sync->{ uidexpunge2 } ) { - if ( Mail::IMAPClient->can( 'uidexpunge' ) ) { +if ( ( $sync->{ delete2 } or $sync->{ delete2duplicates } ) and not defined $sync->{ uidexpunge2 } ) +{ + if ( Mail::IMAPClient->can( 'uidexpunge' ) ) + { myprint( "Info: will act as --uidexpunge2\n" ) ; $sync->{ uidexpunge2 } = 1 ; - }elsif ( not defined $sync->{ expunge2 } ) { + } + elsif ( not defined $sync->{ expunge2 } ) + { myprint( "Info: will act as --expunge2 (no uidexpunge support)\n" ) ; $sync->{ expunge2 } = 1 ; } } -if ( $sync->{ delete1 } and $sync->{ delete2 } ) { +if ( $sync->{ delete1 } and $sync->{ delete2 } ) +{ myprint( "Warning: using --delete1 and --delete2 together is almost always a bad idea. " . "You should probably launch two runs, the first with --delete2 for a strict sync, " . "then the second with --delete1 to remove messages from the source account. " @@ -1815,32 +1916,37 @@ if ( $sync->{ delete1 } and $sync->{ delete2 } ) { exit_clean( $sync, $EX_USAGE ) ; } -if ( $idatefromheader ) { +if ( $sync->{ idatefromheader } ) +{ myprint( 'Turned ON idatefromheader, ', "will set the internal dates on host2 from the 'Date:' header line.\n" ) ; - $syncinternaldates = 0 ; + $sync->{ syncinternaldates } = 0 ; } -if ( $syncinternaldates ) { +if ( $sync->{ syncinternaldates } ) +{ myprint( 'Info: turned ON syncinternaldates, ', "will set the internal dates (arrival dates) on host2 same as host1.\n" ) ; }else{ myprint( "Info: turned OFF syncinternaldates\n" ) ; } -if ( defined $authmd5 and $authmd5 ) { +if ( defined $authmd5 and $authmd5 ) +{ $authmd51 = 1 ; $authmd52 = 1 ; } -if ( defined $authmd51 and $authmd51 ) { +if ( defined $authmd51 and $authmd51 ) +{ $acc1->{ authmech } ||= 'CRAM-MD5' ; } else{ $acc1->{ authmech } ||= $acc1->{ authuser } ? 'PLAIN' : 'LOGIN' ; } -if ( defined $authmd52 and $authmd52 ) { +if ( defined $authmd52 and $authmd52 ) +{ $acc2->{ authmech } ||= 'CRAM-MD5'; } else{ @@ -1912,47 +2018,46 @@ $acc2->{ reconnectretry } = defined $acc2->{ reconnectretry } ? $acc2->{ recon -if ( ! @useheader ) { @useheader = qw( Message-Id Received ) ; } - -# Make a hash %useheader of each --useheader 'key' in uppercase -for ( @useheader ) { $sync->{useheader}->{ uc $_ } = undef } ; - -#myprint( Data::Dumper->Dump( [ \%useheader ] ) ) ; -#exit ; - -myprint( "Host1: IMAP server [$sync->{host1}] port [$sync->{port1}] user [$sync->{user1}]\n" ) ; -myprint( "Host2: IMAP server [$sync->{host2}] port [$sync->{port2}] user [$sync->{user2}]\n" ) ; - -get_password1( $sync ) ; -get_password2( $sync ) ; - -# --dry1 make imapsync not fetching messages from host1, it is on when --dry is on. -# Use --dry --nodry1 to make imapsync fetching messages from host1, -# It is useful when debugging transformation options like --pipemess or --regexmess -$sync->{dry1} = defined $sync->{dry1} ? $sync->{dry1} : $sync->{dry} ; - -$sync->{dry_message} = q{} ; -if( $sync->{dry} ) { - $sync->{dry_message} = "\t(not really since --dry mode)" ; +if ( ! exists( $sync->{ useheader } ) or ! @{ $sync->{ useheader } } ) +{ + @{ $sync->{ useheader } } = qw( Message-Id Received ) ; } -$sync->{ search1 } ||= $search if ( $search ) ; -$sync->{ search2 } ||= $search if ( $search ) ; +# Make a hash ref $sync->{ useheader_h } of each --useheader 'key' in uppercase +for ( @{ $sync->{ useheader } } ) +{ + $sync->{ useheader_h }->{ uc $_ } = undef ; -if ( $disarmreadreceipts ) +} + + +$sync->{ dry1 } = defined $sync->{ dry1 } ? $sync->{ dry1 } : $sync->{ dry } ; + +$sync->{ dry_message } = q{} ; +if( $sync->{ dry } ) +{ + $sync->{ dry_message } = "\t(not really since --dry mode)" ; +} + +$sync->{ search1 } ||= $sync->{ search } if ( $sync->{ search } ) ; +$sync->{ search2 } ||= $sync->{ search } if ( $sync->{ search } ) ; + +if ( $sync->{ disarmreadreceipts } ) { push @regexmess, q{s{\A((?:[^\n]+\r\n)+|)(^Disposition-Notification-To:[^\n]*\n)(\r?\n|.*\n\r?\n)}{$1X-$2$3}ims} ; } -$pipemesscheck = ( defined $pipemesscheck ) ? $pipemesscheck : 1 ; +$sync->{ pipemesscheck } = ( defined $sync->{ pipemesscheck } ) ? $sync->{ pipemesscheck } : 1 ; -if ( @pipemess and $pipemesscheck ) { +if ( @pipemess and $sync->{ pipemesscheck } ) +{ myprint( 'Checking each --pipemess command, ' . join( q{, }, @pipemess ) . ", with an space string. ( Can avoid this check with --nopipemesscheck )\n" ) ; my $string = pipemess( q{ }, @pipemess ) ; # string undef means something was bad. - if ( not ( defined $string ) ) { + if ( not ( defined $string ) ) + { $sync->{nb_errors}++ ; exit_clean( $sync, $EX_USAGE, "Error: one of --pipemess command is bad, check it\n" @@ -1961,26 +2066,14 @@ if ( @pipemess and $pipemesscheck ) { myprint( "Ok with each --pipemess @pipemess\n" ) ; } -if ( $maxlinelengthcmd ) { - myprint( "Checking --maxlinelengthcmd command, - $maxlinelengthcmd, with an space string.\n" - ) ; - my $string = pipemess( q{ }, $maxlinelengthcmd ) ; - # string undef means something was bad. - if ( not ( defined $string ) ) { - $sync->{nb_errors}++ ; - exit_clean( $sync, $EX_USAGE, - "Error: --maxlinelengthcmd command is bad, check it\n" - ) ; - } - myprint( "Ok with --maxlinelengthcmd $maxlinelengthcmd\n" ) ; -} -if ( @regexmess ) { +if ( @regexmess ) +{ my $string = regexmess( q{ } ) ; myprint( "Checking each --regexmess command with an space string.\n" ) ; # string undef means one of the eval regex was bad. - if ( not ( defined $string ) ) { + if ( not ( defined $string ) ) + { #errors_incr( $sync, 'Warning: one of --regexmess option may be bad, check them' ) ; exit_clean( $sync, $EX_USAGE, "Error: one of --regexmess option is bad, check it\n" @@ -1989,11 +2082,13 @@ if ( @regexmess ) { myprint( "Ok with each --regexmess\n" ) ; } -if ( @skipmess ) { +if ( @skipmess ) +{ myprint( "Checking each --skipmess command with an space string.\n" ) ; my $match = skipmess( q{ } ) ; # match undef means one of the eval regex was bad. - if ( not ( defined $match ) ) { + if ( not ( defined $match ) ) + { $sync->{nb_errors}++ ; exit_clean( $sync, $EX_USAGE, "Error: one of --skipmess option is bad, check it\n" @@ -2002,11 +2097,13 @@ if ( @skipmess ) { myprint( "Ok with each --skipmess\n" ) ; } -if ( $sync->{ regexflag } ) { +if ( $sync->{ regexflag } ) +{ myprint( "Checking each --regexflag command with an space string.\n" ) ; my $string = regexflags( $sync, q{ } ) ; # string undef means one of the eval regex was bad. - if ( not ( defined $string ) ) { + if ( not ( defined $string ) ) + { $sync->{nb_errors}++ ; exit_clean( $sync, $EX_USAGE, "Error: one of --regexflag option is bad, check it\n" @@ -2015,48 +2112,64 @@ if ( $sync->{ regexflag } ) { myprint( "Ok with each --regexflag\n" ) ; } -$sync->{imap1} = login_imap( $sync->{host1}, $sync->{port1}, $sync->{user1}, $sync->{password1}, - $sync->{ssl1}, $sync->{tls1}, +if ( $sync->{ host1 } ) +{ + myprint( "Host1: IMAP server [$sync->{ host1 }] port [$sync->{ port1 }] user [" . ( $sync->{ user1 } || '?' ) . "]\n" ) ; +} + +if ( $sync->{ host2 } ) +{ + myprint( "Host2: IMAP server [$sync->{ host2 }] port [$sync->{ port2 }] user [" . ( $sync->{ user2 } || '?' ) . "]\n" ) ; +} + +get_password1( $sync ) ; +get_password2( $sync ) ; + + +$sync->{ imap1 } = login_imap( $sync->{ host1 }, $sync->{ port1 }, $sync->{ user1 }, $sync->{ password1 }, + $sync->{ ssl1 }, $sync->{ tls1 }, $uid1, $split1, $sync->{ acc1 }, $sync ) ; -$sync->{imap2} = login_imap( $sync->{host2}, $sync->{port2}, $sync->{user2}, $sync->{password2}, - $sync->{ssl2}, $sync->{tls2}, +$sync->{ imap2 } = login_imap( $sync->{ host2 }, $sync->{ port2 }, $sync->{ user2 }, $sync->{ password2 }, + $sync->{ ssl2 }, $sync->{ tls2 }, $uid2, $split2, $sync->{ acc2 }, $sync ) ; -$sync->{ debug } and $sync->{imap1} and myprint( 'Host1 Buffer I/O: ', $sync->{imap1}->Buffer(), "\n" ) ; -$sync->{ debug } and $sync->{imap2} and myprint( 'Host2 Buffer I/O: ', $sync->{imap2}->Buffer(), "\n" ) ; +$sync->{ debug } and $sync->{ imap1 } and myprint( 'Host1 Buffer I/O: ', $sync->{ imap1 }->Buffer( ), "\n" ) ; +$sync->{ debug } and $sync->{ imap2 } and myprint( 'Host2 Buffer I/O: ', $sync->{ imap2 }->Buffer( ), "\n" ) ; -if ( ! $sync->{imap1} || ! $sync->{imap2} ) +if ( ! $sync->{ imap1 } || ! $sync->{ imap2 } ) { exit_most_errors( $sync ) ; } - myprint( "Host1: state Authenticated\n" ) ; myprint( "Host2: state Authenticated\n" ) ; -myprint( 'Host1 capability once authenticated: ', join(q{ }, @{ $sync->{imap1}->capability() || [] }), "\n" ) ; +myprint( 'Host1 capability once authenticated: ', join(q{ }, @{ $sync->{ imap1 }->capability() || [] }), "\n" ) ; -#myprint( Data::Dumper->Dump( [ $sync->{imap1} ] ) ) ; -#myprint( "imap4rev1: " . $sync->{imap1}->imap4rev1() . "\n" ) ; +#myprint( Data::Dumper->Dump( [ $sync->{ imap1 } ] ) ) ; +#myprint( "imap4rev1: " . $sync->{ imap1 }->imap4rev1() . "\n" ) ; -myprint( 'Host2 capability once authenticated: ', join(q{ }, @{ $sync->{imap2}->capability() || [] }), "\n" ) ; +myprint( 'Host2 capability once authenticated: ', join(q{ }, @{ $sync->{ imap2 }->capability() || [] }), "\n" ) ; imap_id_stuff( $sync ) ; -#quota( $sync, $sync->{imap1}, 'h1' ) ; # quota on host1 is useless and pollute host2 output. -quota( $sync, $sync->{imap2}, 'h2' ) ; +#quota( $sync, $sync->{ imap1 }, 'h1' ) ; # quota on host1 is useless and pollute host2 output. +quota( $sync, $sync->{ imap2 }, 'h2' ) ; maxsize_setting( $sync ) ; acc_compress_imap( $acc1 ) ; acc_compress_imap( $acc2 ) ; +myprint( "Compression often generates annoying errors, especially with SSL connexions, Gmail, Office365. So, avoid it.\n" ) ; -if ( $sync->{ justlogin } ) { - $sync->{imap1}->logout( ) ; - $sync->{imap2}->logout( ) ; + +if ( $sync->{ justlogin } ) +{ + $sync->{ imap1 }->logout( ) ; + $sync->{ imap2 }->logout( ) ; exit_clean( $sync, $EX_OK, "Exiting because of --justlogin\n" ) ; } @@ -2070,8 +2183,8 @@ $h1_folders_wanted_ct = 0 ; # counter of folders done. # All folders on host1 and host2 -@h1_folders_all = sort $sync->{imap1}->folders( ) ; -@h2_folders_all = sort $sync->{imap2}->folders( ) ; +@h1_folders_all = sort $sync->{ imap1 }->folders( ) ; +@h2_folders_all = sort $sync->{ imap2 }->folders( ) ; myprint( 'Host1: found ', scalar @h1_folders_all , " folders.\n" ) ; myprint( 'Host2: found ', scalar @h2_folders_all , " folders.\n" ) ; @@ -2096,22 +2209,27 @@ private_folders_separators_and_prefixes( ) ; # Make a hash of subscribed folders in both servers. -for ( $sync->{imap1}->subscribed( ) ) { $h1_subscribed_folder{ $_ } = 1 } ; -for ( $sync->{imap2}->subscribed( ) ) { $h2_subscribed_folder{ $_ } = 1 } ; +for ( $sync->{ imap1 }->subscribed( ) ) { $h1_subscribed_folder{ $_ } = 1 } ; +for ( $sync->{ imap2 }->subscribed( ) ) { $h2_subscribed_folder{ $_ } = 1 } ; -if ( defined $sync->{ subfolder1 } ) { +if ( defined $sync->{ subfolder1 } ) +{ subfolder1( $sync ) ; } - - -if ( defined $sync->{ subfolder2 } ) { +# subfolder2() put subfolder2 value in @exclude +if ( defined $sync->{ subfolder2 } ) +{ subfolder2( $sync ) ; } -if ( $fixInboxINBOX and ( my $reg = fix_Inbox_INBOX_mapping( \%h1_folders_all, \%h2_folders_all ) ) ) { +# subfolder2_with_delete2folders() adds --delete2foldersonly m{subfolder2} +subfolder2_with_delete2folders( $sync ) ; + +if ( $sync->{ fixInboxINBOX } and ( my $reg = fix_Inbox_INBOX_mapping( $sync, \%h1_folders_all, \%h2_folders_all ) ) ) +{ push @{ $sync->{ regextrans2 } }, $reg ; } @@ -2119,29 +2237,34 @@ if ( $fixInboxINBOX and ( my $reg = fix_Inbox_INBOX_mapping( \%h1_folders_all, \ if ( ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) or $subscribed - or scalar @folderrec ) + or ( $sync->{ folderrec } and scalar @{ $sync->{ folderrec } } ) + ) { # folders given by option --folder - if ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) { + if ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) + { add_to_requested_folders( @{ $sync->{ folder } } ) ; } # option --subscribed - if ( $subscribed ) { + if ( $subscribed ) + { add_to_requested_folders( keys %h1_subscribed_folder ) ; } # option --folderrec - if ( scalar @folderrec ) { - foreach my $folderrec ( @folderrec ) { - add_to_requested_folders( $sync->{imap1}->folders( $folderrec ) ) ; + if ( $sync->{ folderrec } and scalar @{ $sync->{ folderrec } } ) + { + foreach my $folderrec ( @{ $sync->{ folderrec } } ) { + add_to_requested_folders( $sync->{ imap1 }->folders( $folderrec ) ) ; } } } else { # no include, no folder/subscribed/folderrec options => all folders - if ( not scalar @include ) { + if ( not scalar @include ) + { myprint( "Including all folders found by default. Use --subscribed or --folder or --folderrec or --include to select specific folders. Use --exclude to unselect specific folders.\n" ) ; add_to_requested_folders( @h1_folders_all ) ; } @@ -2149,7 +2272,8 @@ else # consider (optional) includes and excludes -if ( scalar @include ) { +if ( scalar @include ) +{ foreach my $include ( @include ) { # No, do not add /x after the regex, never. # Users would kill you! @@ -2159,7 +2283,8 @@ if ( scalar @include ) { } } -if ( scalar @exclude ) { +if ( scalar @exclude ) +{ foreach my $exclude ( @exclude ) { my @requested_folder = sort keys %requested_folder ; # No, do not add /x after the regex, never. @@ -2179,15 +2304,19 @@ $sync->{h1_folders_wanted} = [ sort_requested_folders( ) ] ; # Remove no selectable folders -if ( $sync->{ checkfoldersexist } ) { +if ( $sync->{ checkfoldersexist } ) +{ my @h1_folders_wanted_exist ; myprint( "Host1: Checking wanted folders exist. Use --nocheckfoldersexist to avoid this check (shared of public namespace targeted).\n" ) ; foreach my $folder ( @{ $sync->{h1_folders_wanted} } ) { ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Checking $folder exists on host1\n" ) ; - if ( ! exists $h1_folders_all{ $folder } ) { + if ( ! exists $h1_folders_all{ $folder } ) + { myprint( "Host1: warning! ignoring folder $folder because it is not in host1 whole folders list.\n" ) ; next ; - }else{ + } + else + { push @h1_folders_wanted_exist, $folder ; } } @@ -2202,7 +2331,7 @@ checkselectable( $sync ) ; -# Bugfix OpenFind folders named like "kk \*123" are in fact "kk *123" (no \) +# Local bugfix. OpenFind folders named like "kk \*123" are in fact "kk *123" (no \) #foreach my $folder ( @{ $sync->{ h1_folders_wanted } } ) #{ # $folder =~ s{ \\\*}{ *}g ; @@ -2211,7 +2340,8 @@ checkselectable( $sync ) ; # this hack is because LWP post does not pass well a hash in the $form parameter # but it does pass well an array -%{ $sync->{f1f2h} } = split_around_equal( @{ $sync->{f1f2} } ) ; +#%{ $sync->{f1f2h} } = split_around_equal( @{ $sync->{f1f2} } ) ; +make_f1f2_array_to_a_hash( $sync ) ; automap( $sync ) ; @@ -2220,7 +2350,8 @@ foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) { my $h2_fold ; $h2_fold = imap2_folder_name( $sync, $h1_fold ) ; $h2_folders_from_1_wanted{ $h2_fold }++ ; - if ( 1 < $h2_folders_from_1_wanted{ $h2_fold } ) { + if ( 1 < $h2_folders_from_1_wanted{ $h2_fold } ) + { $h2_folders_from_1_several{ $h2_fold }++ ; } } @@ -2256,19 +2387,20 @@ END_LISTING myprint( "Host1: folders list (first the raw imap format then the [X] = [Y]):\n", - $sync->{imap1}->list( ), + $sync->{ imap1 }->list( ), "\n", jux_utf8_list( @h1_folders_all ), "\n", "Host2: folders list (first the raw imap format then the [X] = [Y]):\n", - $sync->{imap2}->list( ), + $sync->{ imap2 }->list( ), "\n", jux_utf8_list( @h2_folders_all ), "\n", q{} ) ; -if ( $subscribed ) { +if ( $subscribed ) +{ myprint( 'Host1 subscribed folders list: ', jux_utf8_list( sort keys %h1_subscribed_folder ), "\n", @@ -2279,13 +2411,15 @@ if ( $subscribed ) { @h2_folders_not_in_1 = list_folders_in_2_not_in_1( ) ; -if ( @h2_folders_not_in_1 ) { +if ( @h2_folders_not_in_1 ) +{ myprint( "Folders in host2 not in host1:\n", jux_utf8_list( @h2_folders_not_in_1 ), "\n" ) ; } -if ( keys %{ $sync->{f1f2auto} } ) { +if ( keys %{ $sync->{f1f2auto} } ) +{ myprint( "Folders mapping from --automap feature (use --f1f2 to override any mapping):\n" ) ; foreach my $h1_fold ( keys %{ $sync->{f1f2auto} } ) { my $h2_fold = $sync->{f1f2auto}{$h1_fold} ; @@ -2295,12 +2429,14 @@ if ( keys %{ $sync->{f1f2auto} } ) { myprint( "\n" ) ; } -if ( keys %{ $sync->{f1f2h} } ) { +if ( keys %{ $sync->{f1f2h} } ) +{ myprint( "Folders mapping from --f1f2 options, it overrides --automap:\n" ) ; foreach my $h1_fold ( keys %{ $sync->{f1f2h} } ) { my $h2_fold = $sync->{f1f2h}{$h1_fold} ; my $warn = q{} ; - if ( not exists $h1_folders_all{ $h1_fold } ) { + if ( not exists $h1_folders_all{ $h1_fold } ) + { $warn = "BUT $h1_fold does NOT exist on host1!" ; } myprintf( "%-40s -> %-40s %s\n", @@ -2339,7 +2475,8 @@ if ( $sync->{ checknoabletosearch } ) -if ( $sync->{ foldersizes } ) { +if ( $sync->{ foldersizes } ) +{ foldersizes_at_the_beggining( $sync ) ; #foldersizes_at_the_beggining_old( $sync ) ; @@ -2352,13 +2489,18 @@ if ( $sync->{ justfoldersizes } ) exit_clean( $sync, $EX_OK, "Exiting because of --justfoldersizes\n" ) ; } -$sync->{can_do_stats} = 1 ; +$sync->{ can_do_stats } = 1 ; -if ( $sync->{ delete1emptyfolders } ) { +if ( $sync->{ delete1emptyfolders } ) +{ delete1emptyfolders( $sync ) ; } -delete_folders_in_2_not_in_1( ) if $delete2folders ; + +if ( $sync->{ delete2folders } ) +{ + delete_folders_in_2_not_in_1( $sync, @h2_folders_not_in_1 ) ; +} # folder loop $h1_folders_wanted_nb = scalar @{ $sync->{h1_folders_wanted} } ; @@ -2389,7 +2531,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) myprint( debugmemory( $sync, " at folder loop" ) ) ; # host1 can not be fetched read only, select is needed because of expunge. - select_folder( $sync, $sync->{imap1}, $h1_fold, 'Host1' ) or next FOLDER ; + select_folder( $sync, $sync->{ imap1 }, $h1_fold, 'Host1' ) or next FOLDER ; debugsleep( $sync ) ; @@ -2398,10 +2540,11 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my $h1_msgs_nb ; my $h1_msgs_nb_from_select ; - $h1_msgs_nb_from_select = count_from_select( $sync->{imap1}->History ) ; + $h1_msgs_nb_from_select = count_from_select( $sync->{ imap1 }->History ) ; myprint( "Host1: folder [$h1_fold] has $h1_msgs_nb_from_select messages in total (mentioned by SELECT)\n" ) ; - if ( $sync->{ skipemptyfolders } and 0 == $h1_msgs_nb_from_select ) { + if ( $sync->{ skipemptyfolders } and 0 == $h1_msgs_nb_from_select ) + { myprint( "Host1: skipping empty host1 folder [$h1_fold]\n" ) ; next FOLDER ; } @@ -2409,27 +2552,29 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) # Code added from https://github.com/imapsync/imapsync/issues/95 # Thanks jh1995 # Goal: do not create folder if --search or --max/minage return 0 message. - # even if there are messages by SELECT (no not real empty, empty for the user point of vue). + # even if there are messages by SELECT (not real empty, but empty for the user point of view). if ( $sync->{ skipemptyfolders } or $sync->{ dry } ) { $h1_msgs_all_hash_ref = { } ; - @h1_msgs = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ) ; + @h1_msgs = select_msgs( $sync, $sync->{ imap1 }, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ) ; $h1_msgs_nb = scalar( @h1_msgs ) ; - if ( 0 == $h1_msgs_nb and $sync->{ skipemptyfolders } ) { + if ( 0 == $h1_msgs_nb and $sync->{ skipemptyfolders } ) + { myprint( "Host1: skipping empty host1 folder [$h1_fold] (0 message found by SEARCH)\n" ) ; next FOLDER ; } } - if ( ! exists $h2_folders_all{ $h2_fold } ) { + if ( ! exists $h2_folders_all{ $h2_fold } ) + { # In --dry mode I could count the messages to be transfered instead of 0 # Messages transferred : 0 (could be 0 without dry mode) - if ( ! create_folder( $sync, $sync->{imap2}, $h2_fold, $h1_fold ) ) + if ( ! create_folder( $sync, $sync->{ imap2 }, $h2_fold, $h1_fold ) ) { if ( $sync->{ dry } ) { - $nb_msg_skipped_dry_mode += $h1_msgs_nb ; + $sync->{ nb_msg_skipped_dry_mode } += $h1_msgs_nb ; } next FOLDER ; } @@ -2439,24 +2584,24 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) # Sometimes the folder on host2 is listed (it exists) but is # not selectable but becomes selectable by a create (Gmail) - select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) - or ( create_folder( $sync, $sync->{imap2}, $h2_fold, $h1_fold ) - and select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) ) + select_folder( $sync, $sync->{ imap2 }, $h2_fold, 'Host2' ) + or ( create_folder( $sync, $sync->{ imap2 }, $h2_fold, $h1_fold ) + and select_folder( $sync, $sync->{ imap2 }, $h2_fold, 'Host2' ) ) or next FOLDER ; - my @select_results = $sync->{imap2}->Results( ) ; + my @select_results = $sync->{ imap2 }->Results( ) ; my $h2_fold_nb_messages = count_from_select( @select_results ) ; myprint( "Host2: folder [$h2_fold] has $h2_fold_nb_messages messages in total (mentioned by SELECT)\n" ) ; - my $permanentflags2 = permanentflags( $sync, @select_results ) ; - myprint( "Host2: folder [$h2_fold] permanentflags: $permanentflags2\n" ) ; + $sync->{ permanentflags2 } = permanentflags( $sync, @select_results ) ; + myprint( "Host2: folder [$h2_fold] permanentflags: $sync->{ permanentflags2 }\n" ) ; if ( $sync->{ expunge1 } ) { - myprint( "Host1: Expunging $h1_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) + myprint( "Host1: Expunging $h1_fold $sync->{ dry_message }\n" ) ; + if ( ! $sync->{ dry } ) { - $sync->{imap1}->expunge( ) ; + $sync->{ imap1 }->expunge( ) ; } } @@ -2464,7 +2609,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) and not exists $h2_subscribed_folder{ $h2_fold } ) { myprint( "Host2: Subscribing to folder $h2_fold\n" ) ; - if ( ! $sync->{dry} ) { $sync->{imap2}->subscribe( $h2_fold ) } ; + if ( ! $sync->{ dry } ) { $sync->{ imap2 }->subscribe( $h2_fold ) } ; } next FOLDER if ( $sync->{ justfolders } ) ; @@ -2475,7 +2620,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) if ( ! defined $h1_msgs_nb ) { $h1_msgs_all_hash_ref = { } ; - @h1_msgs = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ); + @h1_msgs = select_msgs( $sync, $sync->{ imap1 }, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ); $h1_msgs_nb = scalar @h1_msgs ; }else{ # select_msgs already done. @@ -2484,18 +2629,18 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages\n" ) ; - ( $sync->{ debug } or $debuglist ) and myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages, LIST gives: @h1_msgs\n" ) ; + ( $sync->{ debug } or $sync->{ debuglist } ) and myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages, LIST gives: @h1_msgs\n" ) ; $sync->{ debug } and myprint( "Host1: selecting messages of folder [$h1_fold] took ", timenext( $sync ), " s\n" ) ; my $h2_msgs_all_hash_ref = { } ; - my @h2_msgs = select_msgs( $sync->{imap2}, $h2_msgs_all_hash_ref, $sync->{ search2 }, $sync->{abletosearch2}, $h2_fold ) ; + my @h2_msgs = select_msgs( $sync, $sync->{ imap2 }, $h2_msgs_all_hash_ref, $sync->{ search2 }, $sync->{abletosearch2}, $h2_fold ) ; if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } my $h2_msgs_nb = scalar @h2_msgs ; myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages\n" ) ; - ( $sync->{ debug } or $debuglist ) and myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages, LIST gives: @h2_msgs\n" ) ; + ( $sync->{ debug } or $sync->{ debuglist } ) and myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages, LIST gives: @h2_msgs\n" ) ; $sync->{ debug } and myprint( "Host2: selecting messages of folder [$h2_fold] took ", timenext( $sync ), " s\n" ) ; my $cache_base = "$sync->{ tmpdir }/imapsync_cache/" ; @@ -2503,16 +2648,17 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) "$sync->{host1}/$sync->{user1}/$sync->{host2}/$sync->{user2}", $h1_fold, $h2_fold ) ; my ( $cache_1_2_ref, $cache_2_1_ref ) = ( {}, {} ) ; - my $h1_uidvalidity = $sync->{imap1}->uidvalidity( ) || q{} ; - my $h2_uidvalidity = $sync->{imap2}->uidvalidity( ) || q{} ; + my $h1_uidvalidity = $sync->{ imap1 }->uidvalidity( ) || q{} ; + my $h2_uidvalidity = $sync->{ imap2 }->uidvalidity( ) || q{} ; if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - if ( $usecache ) { + if ( $sync->{ usecache } ) + { myprint( "Local cache directory: $cache_dir ( " . length( $cache_dir ) . " characters long )\n" ) ; mkpath( "$cache_dir" ) ; ( $cache_1_2_ref, $cache_2_1_ref ) - = get_cache( $cache_dir, \@h1_msgs, \@h2_msgs, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; + = get_cache( $sync, $cache_dir, \@h1_msgs, \@h2_msgs, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; myprint( 'CACHE h1 h2: ', scalar keys %{ $cache_1_2_ref } , " files\n" ) ; $sync->{ debug } and myprint( '[', map ( { "$_->$cache_1_2_ref->{$_} " } keys %{ $cache_1_2_ref } ), " ]\n" ) ; @@ -2541,38 +2687,42 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my @h2_msgs_delete2_not_in_cache = () ; %h1_msgs_copy_by_uid = ( ) ; - if ( $useuid ) { + if ( $sync->{ useuid } ) + { # use uid so we have to avoid getting header @h1_msgs_copy_by_uid{ @h1_msgs_not_in_cache } = ( ) ; - @h2_msgs_delete2_not_in_cache = @h2_msgs_not_in_cache if $usecache ; + @h2_msgs_delete2_not_in_cache = @h2_msgs_not_in_cache if $sync->{ usecache } ; @h1_msgs_not_in_cache = ( ) ; @h2_msgs_not_in_cache = ( ) ; #myprint( "delete2: @h2_msgs_delete2_not_in_cache\n" ) ; } - - $sync->{ debug } and myprint( "Host1: parsing headers of folder [$h1_fold]\n" ) ; - - my ($h1_heads_ref, $h1_fir_ref) = ({}, {}); - $h1_heads_ref = $sync->{imap1}->parse_headers([@h1_msgs_not_in_cache], @useheader) if (@h1_msgs_not_in_cache); + + if ( $sync->{ debug } or ( 5000 <= scalar( @h1_msgs_not_in_cache ) ) ) + { + myprint( "Host1: parsing headers of folder [$h1_fold]. It can take time for huge folders. Be patient.\n" ) ; + } + + my ( $h1_heads_ref, $h1_fir_ref ) = ( {}, {} ) ; + $h1_heads_ref = $sync->{ imap1 }->parse_headers( [ @h1_msgs_not_in_cache ], @{ $sync->{ useheader } } ) if ( @h1_msgs_not_in_cache ) ; $sync->{ debug } and myprint( "Host1: parsing headers of folder [$h1_fold] took ", timenext( $sync ), " s\n" ) ; - @{ $h1_fir_ref }{@h1_msgs} = ( undef ) ; + @{ $h1_fir_ref }{ @h1_msgs } = ( undef ) ; - $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold]\n" ) ; + $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold]\n" ) ; my @h1_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h1_common_fetch_param, 'X-GM-LABELS' ; } - if ( $sync->{abletosearch1} ) + if ( $sync->{ abletosearch1 } ) { - $h1_fir_ref = $sync->{imap1}->fetch_hash( \@h1_msgs, @h1_common_fetch_param, $h1_fir_ref ) + $h1_fir_ref = $sync->{ imap1 }->fetch_hash( \@h1_msgs, @h1_common_fetch_param, $h1_fir_ref ) if ( @h1_msgs ) ; } else { - my $fetch_hash_uids = $fetch_hash_set || "1:*" ; - $h1_fir_ref = $sync->{imap1}->fetch_hash( $fetch_hash_uids, @h1_common_fetch_param, $h1_fir_ref ) + my $fetch_hash_uids = $sync->{ fetch_hash_set } || "1:*" ; + $h1_fir_ref = $sync->{ imap1 }->fetch_hash( $fetch_hash_uids, @h1_common_fetch_param, $h1_fir_ref ) if ( @h1_msgs ) ; } @@ -2580,7 +2730,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) if ( ! $h1_fir_ref ) { my $error = join( q{}, "Host1: folder $h1_fold : Could not fetch_hash ", - scalar @h1_msgs, ' msgs: ', $sync->{imap1}->LastError || q{}, "\n" ) ; + scalar @h1_msgs, ' msgs: ', $sync->{ imap1 }->LastError || q{}, "\n" ) ; errors_incr( $sync, $error ) ; next FOLDER ; } @@ -2588,7 +2738,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my @h1_msgs_duplicate; foreach my $m ( @h1_msgs_not_in_cache ) { - my $rc = parse_header_msg( $sync, $sync->{imap1}, $m, $h1_heads_ref, $h1_fir_ref, 'Host1', \%h1_hash ) ; + my $rc = parse_header_msg( $sync, $sync->{ imap1 }, $m, $h1_fold, $h1_heads_ref, $h1_fir_ref, 'Host1', \%h1_hash ) ; if ( ! defined $rc ) { my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0; @@ -2605,7 +2755,8 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0; $sync->{ acc1 }->{ nb_msg_duplicate } += 1; - if ( ! $sync->{ syncduplicates } ) { + if ( ! $sync->{ syncduplicates } ) + { $sync->{ nb_msg_skipped } += 1 ; $sync->{ h1_nb_msg_processed } +=1 ; } @@ -2619,30 +2770,34 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) $sync->{ debug } and myprint( 'Host1: whole time parsing headers took ', timenext( $sync ), " s\n" ) ; - - # Getting headers and metada can be so long that host2 might be disconnected here if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - $sync->{ debug } and myprint( "Host2: parsing headers of folder [$h2_fold]\n" ) ; + if ( $sync->{ debug } or ( 5000 <= scalar( @h2_msgs_not_in_cache ) ) ) + { + myprint( "Host2: parsing headers of folder [$h2_fold]. It can take time for huge folders. Be patient.\n" ) ; + } - my ($h2_heads_ref, $h2_fir_ref) = ( {}, {} ); - $h2_heads_ref = $sync->{imap2}->parse_headers([@h2_msgs_not_in_cache], @useheader) if (@h2_msgs_not_in_cache); + my ( $h2_heads_ref, $h2_fir_ref ) = ( {}, {} ); + $h2_heads_ref = $sync->{ imap2 }->parse_headers( [ @h2_msgs_not_in_cache ], @{ $sync->{ useheader } } ) if ( @h2_msgs_not_in_cache ); $sync->{ debug } and myprint( "Host2: parsing headers of folder [$h2_fold] took ", timenext( $sync ), " s\n" ) ; $sync->{ debug } and myprint( "Host2: getting flags idate and sizes of folder [$h2_fold]\n" ) ; - @{ $h2_fir_ref }{@h2_msgs} = ( ); # fetch_hash can select by uid with last arg as ref + @{ $h2_fir_ref }{ @h2_msgs } = ( ); # fetch_hash can select by uid with last arg as ref my @h2_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h2_common_fetch_param, 'X-GM-LABELS' ; } - if ( $sync->{abletosearch2} and scalar( @h2_msgs ) ) { - $h2_fir_ref = $sync->{imap2}->fetch_hash( \@h2_msgs, @h2_common_fetch_param, $h2_fir_ref) ; - }else{ - my $fetch_hash_uids = $fetch_hash_set || "1:*" ; - $h2_fir_ref = $sync->{imap2}->fetch_hash( $fetch_hash_uids, @h2_common_fetch_param, $h2_fir_ref ) + if ( $sync->{ abletosearch2 } and scalar( @h2_msgs ) ) + { + $h2_fir_ref = $sync->{ imap2 }->fetch_hash( \@h2_msgs, @h2_common_fetch_param, $h2_fir_ref ) ; + } + else + { + my $fetch_hash_uids = $sync->{ fetch_hash_set } || "1:*" ; + $h2_fir_ref = $sync->{ imap2 }->fetch_hash( $fetch_hash_uids, @h2_common_fetch_param, $h2_fir_ref ) if ( @h2_msgs ) ; } @@ -2650,12 +2805,15 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my @h2_msgs_duplicate; foreach my $m (@h2_msgs_not_in_cache) { - my $rc = parse_header_msg( $sync, $sync->{imap2}, $m, $h2_heads_ref, $h2_fir_ref, 'Host2', \%h2_hash ) ; + my $rc = parse_header_msg( $sync, $sync->{ imap2 }, $m, $h2_fold, $h2_heads_ref, $h2_fir_ref, 'Host2', \%h2_hash ) ; my $h2_size = $h2_fir_ref->{$m}->{'RFC822.SIZE'} || 0 ; - if (! defined $rc ) { + if (! defined $rc ) + { myprint( "Host2: $h2_fold/$m size $h2_size ignored (no wanted headers so we ignore this message)\n" ) ; $h2_nb_msg_noheader += 1 ; - } elsif( 0 == $rc ) { + } + elsif( 0 == $rc ) + { # duplicate $sync->{ acc2 }->{ nb_msg_duplicate } += 1 ; push @h2_msgs_duplicate, $m ; @@ -2691,75 +2849,87 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) # Deletions on account2. - if( $sync->{ delete2duplicates } and not exists $h2_folders_from_1_several{ $h2_fold } ) { + if( $sync->{ delete2duplicates } and not exists $h2_folders_from_1_several{ $h2_fold } ) + { my @h2_expunge ; foreach my $h2_msg ( @h2_msgs_duplicate ) { - myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [duplicate] on host2 $sync->{dry_message}\n" ) ; + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [duplicate] on host2 $sync->{ dry_message }\n" ) ; push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; - if ( ! $sync->{ dry } ) { + if ( ! $sync->{ dry } ) + { $sync->{ imap2 }->delete_message( $h2_msg ) ; $sync->{ acc2 }->{ nb_msg_deleted } += 1 ; } } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $sync->{ expunge2 } ) { - myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + if( @h2_expunge and not $sync->{ expunge2 } ) + { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->uidexpunge( \@h2_expunge ) if ! $sync->{ dry } ; } if ( $sync->{ expunge2 } ){ - myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + myprint( "Host2: Expunging folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->expunge( ) if ! $sync->{ dry } ; } } - if( $sync->{ delete2 } and not exists $h2_folders_from_1_several{ $h2_fold } ) { + if( $sync->{ delete2 } and not exists $h2_folders_from_1_several{ $h2_fold } ) + { # No host1 folders f1a f1b ... going all to same f2 (via --regextrans2) my @h2_expunge; foreach my $m_id (@h2_hash_keys_sorted_by_uid) { #myprint( "$m_id " ) ; - if ( ! exists $h1_hash{$m_id} ) { + if ( ! exists $h1_hash{$m_id} ) + { my $h2_msg = $h2_hash{$m_id}{'m'}; my $h2_flags = $h2_hash{$m_id}{'F'} || q{}; my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0; - myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted on host2 [$m_id] $sync->{dry_message}\n" ) + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted on host2 [$m_id] $sync->{ dry_message }\n" ) if ! $isdel; push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; - if ( ! ( $sync->{ dry } or $isdel ) ) { + if ( ! ( $sync->{ dry } or $isdel ) ) + { $sync->{ imap2 }->delete_message( $h2_msg ); $sync->{ acc2 }->{ nb_msg_deleted } += 1; } } } foreach my $h2_msg ( @h2_msgs_delete2_not_in_cache ) { - myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [not in cache] on host2 $sync->{dry_message}\n" ) ; + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [not in cache] on host2 $sync->{ dry_message }\n" ) ; push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; - if ( ! $sync->{dry} ) { + if ( ! $sync->{ dry } ) + { $sync->{ imap2 }->delete_message( $h2_msg ); $sync->{ acc2 }->{ nb_msg_deleted } += 1; } } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $sync->{ expunge2 } ) { - myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + if( @h2_expunge and not $sync->{ expunge2 } ) + { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->uidexpunge( \@h2_expunge ) if ! $sync->{ dry } ; } - if ( $sync->{ expunge2 } ) { - myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + if ( $sync->{ expunge2 } ) + { + myprint( "Host2: Expunging folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->expunge( ) if ! $sync->{ dry } ; } } - if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } ) { + if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } ) + { myprint( "Host2: folder $h2_fold $h2_folders_from_1_several{ $h2_fold } folders left to sync there\n" ) ; my @h2_expunge; foreach my $m_id ( @h2_hash_keys_sorted_by_uid ) { my $h2_msg = $h2_hash{ $m_id }{ 'm' } ; - if ( ! exists $h1_hash{ $m_id } ) { + if ( ! exists $h1_hash{ $m_id } ) + { my $h2_flags = $h2_hash{ $m_id }{ 'F' } || q{} ; my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0 ; - if ( ! $isdel ) { + if ( ! $isdel ) + { $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [$m_id]\n" ) ; $uid_candidate_for_deletion{ $h2_fold }{ $h2_msg }++ ; } @@ -2779,17 +2949,22 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) } - if ( 0 == $h2_folders_from_1_several{ $h2_fold } ) { + if ( 0 == $h2_folders_from_1_several{ $h2_fold } ) + { # last host1 folder going to $h2_fold myprint( "Last host1 folder going to $h2_fold\n" ) ; foreach my $h2_msg ( keys %{ $uid_candidate_for_deletion{ $h2_fold } } ) { $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion\n" ) ; - if ( exists $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg } ) { + if ( exists $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg } ) + { $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg canceled deletion\n" ) ; - }else{ - myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted $sync->{dry_message}\n" ) ; + } + else + { + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted $sync->{ dry_message }\n" ) ; push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; - if ( ! $sync->{ dry} ) { + if ( ! $sync->{ dry} ) + { $sync->{ imap2 }->delete_message( $h2_msg ) ; $sync->{ acc2 }->{ nb_msg_deleted } += 1 ; } @@ -2798,19 +2973,21 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $sync->{ expunge2 } ) { - myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + if ( @h2_expunge and not $sync->{ expunge2 } ) + { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->uidexpunge( \@h2_expunge ) if ! $sync->{ dry } ; } - if ( $sync->{ expunge2 } ) { - myprint( "Host2: Expunging host2 folder $h2_fold $sync->{dry_message}\n" ) ; - $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + if ( $sync->{ expunge2 } ) + { + myprint( "Host2: Expunging host2 folder $h2_fold $sync->{ dry_message }\n" ) ; + $sync->{ imap2 }->expunge( ) if ! $sync->{ dry } ; } $h2_folders_from_1_several{ $h2_fold }-- ; } - my $h2_uidnext = $sync->{imap2}->uidnext( $h2_fold ) ; + my $h2_uidnext = $sync->{ imap2 }->uidnext( $h2_fold ) ; $sync->{ debug } and myprint( "Host2: uidnext is $h2_uidnext\n" ) ; $h2_uidguess = $h2_uidnext ; @@ -2827,16 +3004,17 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) my $h1_msg = $h1_hash{$m_id}{'m'}; my $h1_idate = $h1_hash{$m_id}{'D'}; - #my $labels = labels( $sync->{imap1}, $h1_msg ) ; + #my $labels = labels( $sync->{ imap1 }, $h1_msg ) ; #print "LABELS: $labels\n" ; if ( ( not exists $h2_hash{ $m_id } ) and ( not ( exists $sync->{ h2_folders_of_md5 }->{ $m_id } ) - or not $skipcrossduplicates ) ) + or not $sync->{ skipcrossduplicates } ) ) { # copy - my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ; - if ( $h2_msg and $sync->{ delete1 } and not $sync->{ expungeaftereach } ) { + my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $cache_dir ) ; + if ( $h2_msg and $sync->{ delete1 } and not $sync->{ expungeaftereach } ) + { # not expunged push @h1_msgs_to_delete, $h1_msg ; } @@ -2849,12 +3027,14 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) } # - if( $sync->{ delete2 } and ( exists $h2_folders_from_1_several{ $h2_fold } ) and $h2_msg ) { + if ( $sync->{ delete2 } and ( exists $h2_folders_from_1_several{ $h2_fold } ) and $h2_msg ) + { myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n" ) ; $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; } - if ( total_bytes_max_reached( $sync ) ) { + if ( total_bytes_max_reached( $sync ) ) + { # Still a bug when using --delete1 --noexpungeaftereach # same thing below on all total_bytes_max_reached! last FOLDER ; @@ -2868,9 +3048,9 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) { my $h2_msg = $h2_hash{$m_id}{'m'} ; $sync->{ debug } and myprint( "Host1: found that msg $h1_fold/$h1_msg equals Host2 $h2_fold/$h2_msg\n" ) ; - if ( $usecache ) + if ( $sync->{ usecache } ) { - $debugcache and myprint( "touch $cache_dir/${h1_msg}_$h2_msg\n" ) ; + $sync->{ debugcache } and myprint( "touch $cache_dir/${h1_msg}_$h2_msg\n" ) ; touch( "$cache_dir/${h1_msg}_$h2_msg" ) or croak( "Couldn't touch $cache_dir/${h1_msg}_$h2_msg" ) ; } @@ -2878,7 +3058,7 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) elsif( exists $sync->{ h2_folders_of_md5 }->{ $m_id } ) { my @folders_dup = keys %{ $sync->{ h2_folders_of_md5 }->{ $m_id } } ; - ( $sync->{ debug } or $debugcrossduplicates ) and myprint( "Host1: found that msg $h1_fold/$h1_msg is also in Host2 folders @folders_dup\n" ) ; + ( $sync->{ debug } or $sync->{ debugcrossduplicates } ) and myprint( "Host1: found that msg $h1_fold/$h1_msg is also in Host2 folders @folders_dup\n" ) ; $sync->{ h2_nb_msg_crossdup } +=1 ; } $sync->{ total_bytes_skipped } += $h1_size ; @@ -2886,11 +3066,13 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) $sync->{ h1_nb_msg_processed } +=1 ; } - if ( exists $h2_hash{ $m_id } ) { + if ( exists $h2_hash{ $m_id } ) + { #$debug and myprint( "MESSAGE $m_id\n" ) ; my $h2_msg = $h2_hash{$m_id}{'m'}; - if ( $sync->{resyncflags} ) { - sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ; + if ( $sync->{resyncflags} ) + { + sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $h1_fir_ref, $h2_fir_ref ) ; } # Good my $h2_size = $h2_hash{$m_id}{'s'}; @@ -2905,7 +3087,8 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - if ( $sync->{ delete1 } ) { + if ( $sync->{ delete1 } ) + { push @h1_msgs_to_delete, $h1_msg ; } } @@ -2922,10 +3105,10 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) foreach my $h1_msg ( @h1_msgs_in_cache ) { my $h2_msg = $cache_1_2_ref->{ $h1_msg } ; - $debugcache and myprint( "cache messages update flags $h1_msg->$h2_msg\n" ) ; - if ( $sync->{resyncflags} ) + $sync->{ debugcache } and myprint( "cache messages update flags $h1_msg->$h2_msg\n" ) ; + if ( $sync->{ resyncflags } ) { - sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ; + sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $h1_fir_ref, $h2_fir_ref ) ; } my $h1_size = $h1_fir_ref->{ $h1_msg }->{ 'RFC822.SIZE' } || 0 ; $sync->{ total_bytes_skipped } += $h1_size; @@ -2945,8 +3128,9 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) $sync->{ debug } and myprint( "Copy by uid $h1_fold/$h1_msg\n" ) ; if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ; - if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } and $h2_msg ) { + my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $cache_dir ) ; + if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } and $h2_msg ) + { myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n" ) ; $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; } @@ -2954,12 +3138,12 @@ FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) } if ( $sync->{ expunge1 } ){ - myprint( "Host1: Expunging folder $h1_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { $sync->{imap1}->expunge( ) } ; + myprint( "Host1: Expunging folder $h1_fold $sync->{ dry_message }\n" ) ; + if ( ! $sync->{ dry } ) { $sync->{ imap1 }->expunge( ) } ; } if ( $sync->{ expunge2 } ){ - myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { $sync->{imap2}->expunge( ) } ; + myprint( "Host2: Expunging folder $h2_fold $sync->{ dry_message }\n" ) ; + if ( ! $sync->{ dry } ) { $sync->{ imap2 }->expunge( ) } ; } $sync->{ debug } and myprint( 'Time: ', timenext( $sync ), " s\n" ) ; } @@ -2968,14 +3152,16 @@ eta_print( $sync ) ; myprint( "++++ End looping on each folder\n" ) ; -if ( $sync->{ delete1 } and $sync->{ delete1emptyfolders } ) { +if ( $sync->{ delete1 } and $sync->{ delete1emptyfolders } ) +{ delete1emptyfolders( $sync ) ; } ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( 'Time: ', timenext( $sync ), " s\n" ) ; -if ( $sync->{ foldersizesatend } ) { +if ( $sync->{ foldersizesatend } ) +{ myprint( << 'END_SIZE' ) ; Folders sizes after the synchronization. @@ -2985,9 +3171,6 @@ END_SIZE foldersizesatend( $sync ) ; } -#$sync->{imap1}->State( 0 ); # Unconnected -if ( ! lost_connection( $sync, $sync->{imap1}, "for host1 [$sync->{host1}]" ) ) { $sync->{imap1}->logout( ) ; } -if ( ! lost_connection( $sync, $sync->{imap2}, "for host2 [$sync->{host2}]" ) ) { $sync->{imap2}->logout( ) ; } do_and_print_stats( $sync ) ; @@ -2998,11 +3181,16 @@ if ( $sync->{ nb_errors } ) } -if ( $sync->{ testslive } or $sync->{ testslive6 } ) +if ( $sync->{ testslive } or $sync->{ testslive6 } or $sync->{ testslive2 } ) { tests_live_result( $sync->{ nb_errors } ) ; } +final_emails_reports( $sync ) ; + +#$sync->{ imap1 }->State( 0 ); # Unconnected +if ( ! lost_connection( $sync, $sync->{ imap1 }, "for host1 [$sync->{host1}]" ) ) { $sync->{ imap1 }->logout( ) ; } +if ( ! lost_connection( $sync, $sync->{ imap2 }, "for host2 [$sync->{host2}]" ) ) { $sync->{ imap2 }->logout( ) ; } if ( $sync->{ nb_errors } ) { @@ -3015,7 +3203,7 @@ else } return ; -} +} # END of sub single_sync @@ -3034,6 +3222,7 @@ sub myprintf return ; } +# What did I have in mind with this redundant/equivalent function? sub mysprintf { my( $format, @list ) = @ARG ; @@ -3137,16 +3326,16 @@ sub tests_print_output_if_needed output( $mysync, "Hello\n" ) ; is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: Hello => Hello' ) ; - + $mysync->{ dockercontext } = 1 ; is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: dockercontext + Hello => Hello' ) ; - + $mysync->{ version } = 1 ; is( q{}, print_output_if_needed( $mysync ), 'print_output_if_needed: dockercontext + Hello + --version => ""' ) ; - + $mysync->{ dockercontext } = 0 ; is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: Hello + --version => Hello' ) ; - + note( 'Leaving tests_print_output_if_needed()' ) ; return ; } @@ -3154,11 +3343,11 @@ sub tests_print_output_if_needed sub print_output_if_needed { - + my $mysync = shift @ARG ; if ( ! defined $mysync ) { return ; } my $output = output( $mysync ) ; - + if ( $mysync->{ version } && under_docker_context( $mysync ) ) { return q{} ; @@ -3168,10 +3357,50 @@ sub print_output_if_needed myprint( $output ) ; return $output ; } - + } +sub stderr_to_stdout +{ + my $mysync = shift @ARG ; + if ( $mysync->{ tee} ) + { + *STDERR = *${ $mysync->{ tee } }{ IO } ; + } + else + { + *STDERR = *STDOUT ; + } + return ; +} + + + +sub determine_delete2duplicates +{ + my $mysync = shift @ARG ; + + if ( defined $mysync->{ delete2duplicates } ) + { + return $mysync->{ delete2duplicates } ; + } + + if ( $mysync->{ syncduplicates } ) + { + return 0 ; + } + + if ( $sync->{ delete2 } ) + { + return 1 ; + } + + return ; +} + + + sub define_pidfile { @@ -3189,7 +3418,7 @@ sub define_pidfile my $pidfile_basename ; - if ( $ENV{ 'NET_SERVER_SOFTWARE' } and ( $ENV{ 'NET_SERVER_SOFTWARE' } =~ /Net::Server::HTTP/ ) ) + if ( not under_docker_context( $mysync ) and under_Net_Server_HTTP( ) ) { # under local webserver $pidfile_basename = 'imapsync' . '_' . $account1_filtered . '_' . $account2_filtered . '.pid' ; @@ -3221,8 +3450,8 @@ sub abortfile return $abortfile ; } -sub tests_kill_zero -{ +sub tests_kill_zero +{ note( 'Entering tests_kill_zero()' ) ; @@ -3234,7 +3463,7 @@ sub tests_kill_zero is( 1, kill( 'ZERO', $PROCESS_ID ), "kill ZERO : myself $PROCESS_ID => 1" ) ; is( 2, kill( 'ZERO', $PROCESS_ID, $PROCESS_ID ), "kill ZERO : myself $PROCESS_ID $PROCESS_ID => 2" ) ; - if ( (-e '/.dockerenv' ) or ( 0 == $EFFECTIVE_USER_ID) ) + if ( skip_docker( ) or ( 0 == $EFFECTIVE_USER_ID) ) { is( 1, kill( 'ZERO', 1 ), "kill ZERO : pid 1 => 1 (docker context or root)" ) ; is( 2, kill( 'ZERO', $PROCESS_ID, 1 ), "kill ZERO : myself + pid 1, $PROCESS_ID 1 => 2 (docker context or root)" ) ; @@ -3285,7 +3514,7 @@ sub tests_kill_zero note( 'Leaving tests_kill_zero()' ) ; return ; -} +} @@ -3394,26 +3623,31 @@ sub tests_killpid_by_brother sub killpid { - my $pidtokill = shift ; + my $pidtokill = shift @ARG ; - if ( ! $pidtokill ) { + if ( ! $pidtokill ) + { myprint( "No process to kill.\n" ) ; return ; } - if ( $PROCESS_ID == $pidtokill ) { + if ( $PROCESS_ID == $pidtokill ) + { myprint( "I will not kill myself pid $PROCESS_ID via killpid. Sractch it!\n" ) ; return ; } # First ask for suicide - if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) { + if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) + { myprint( "Sending signal QUIT to PID $pidtokill \n" ) ; kill 'QUIT', $pidtokill ; sleep 3 ; waitpid( $pidtokill, WNOHANG) ; - }else{ + } + else + { myprint( "Can not send signal kill ZERO to PID $pidtokill.\n" ) ; return ; } @@ -3421,20 +3655,26 @@ sub killpid #while ( waitpid( $pidtokill, WNOHANG) > 0 ) { } ; # Then murder - if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) { + if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) + { myprint( "Sending signal KILL to PID $pidtokill \n" ) ; kill 'KILL', $pidtokill ; sleep 1 ; waitpid( $pidtokill, WNOHANG) ; - }else{ + } + else + { myprint( "Process PID $pidtokill ended.\n" ) ; return 1; } # Well ... - if ( kill( 'ZERO', $pidtokill ) or ( 'xMSWin32' eq $OSNAME ) ) { + if ( kill( 'ZERO', $pidtokill ) or ( 'xMSWin32' eq $OSNAME ) ) + { myprint( "Process PID $pidtokill seems still there. Can not do much.\n" ) ; return ; - }else{ + } + else + { myprint( "Process PID $pidtokill ended.\n" ) ; return 1; } @@ -3454,31 +3694,33 @@ sub tests_abort -sub abort +sub abort { my $mysync = shift @ARG ; myprint( "In abort\n" ) ; if ( not $mysync ) { return ; } - if ( ! -r $mysync->{pidfile} ) { - myprint( "In abort: Can not read pidfile $mysync->{pidfile}\n" ) ; + if ( ! -r $mysync->{ pidfile } ) + { + myprint( "In abort: Can not read pidfile $mysync->{ pidfile }\n" ) ; return ; } - my $pidtokill = firstline( $mysync->{pidfile} ) ; - if ( ! $pidtokill ) { - myprint( "In abort: No process to abort in $mysync->{pidfile}\n" ) ; + my $pidtokill = firstline( $mysync->{ pidfile } ) ; + if ( ! $pidtokill ) + { + myprint( "In abort: No process to abort in $mysync->{ pidfile }\n" ) ; return ; } if ( ! match_a_pid_number( $pidtokill ) ) { - myprint( "In abort: pid $pidtokill in $mysync->{pidfile} is not a pid number\n" ) ; + myprint( "In abort: pid $pidtokill in $mysync->{ pidfile } is not a pid number\n" ) ; return ; } - if ( $mysync->{abortbyfile} ) + if ( $mysync->{ abortbyfile } ) { abortbyfile( $mysync, $pidtokill ) ; } @@ -3487,7 +3729,7 @@ sub abort killpid( $pidtokill ) ; } return ; -} +} sub abortbyfile { @@ -3531,8 +3773,8 @@ sub tests_under_docker_context sub under_docker_context { - my $mysync = shift ; - + my $mysync = shift @ARG ; + if ( ! defined $mysync ) { return ; } if ( defined $mysync->{ dockercontext } ) @@ -3540,7 +3782,7 @@ sub under_docker_context return( $mysync->{ dockercontext } ) ; } - if ( -e '/.dockerenv' ) + if ( -e '/Dockerfile' ) { return 1 ; } @@ -3554,42 +3796,30 @@ sub under_docker_context sub docker_context -{ - my $mysync = shift ; +{ + my $mysync = shift @ARG ; if ( ! under_docker_context( $mysync ) ) { return ; } - output( $mysync, "Docker context detected with the file /.dockerenv\n" ) ; - # No pidfile by default + output( $mysync, "Docker context detected with the file /Dockerfile, or forced\n" ) ; - $mysync->{ pidfile } = defined( $mysync->{ pidfile } ) ? $mysync->{ pidfile } : q{} ; - # No log by default - if ( defined( $mysync->{ log } ) ) - { - output( $mysync, "Logging in Docker context. Be sure you added access to it with a mount or similar. See https://docs.docker.com/storage/volumes/\n" ) ; - } - else - { - output( $mysync, "No log by default in Docker context. Use --log to trigger logging to the logfile.\n" ) ; - $mysync->{ log } = 0 ; - } # In case something is written relatively to . - my $tmp_dir = "/var/tmp/uid_$EFFECTIVE_USER_ID" ; - mkpath( $tmp_dir ) ; # silly? No. it is for imapsync --version being ok. - do_valid_directory( $tmp_dir ) ; - output( $mysync, "Changing current directory to $tmp_dir\n" ) ; - chdir $tmp_dir ; - + my $tmpdir = "/var/tmp/uid_$EFFECTIVE_USER_ID" ; + mkpath( $tmpdir ) ; # silly? No. it is for imapsync --version being ok. + make_valid_directory( $mysync, $tmpdir ) ; + output( $mysync, "Changing current directory from " . getcwd() . " to $tmpdir\n" ) ; + chdir $tmpdir ; + return ; } sub cgibegin { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! under_cgi_context( $mysync ) ) { return ; } require CGI ; CGI->import( qw( -no_debug -utf8 ) ) ; @@ -3631,18 +3861,19 @@ sub tests_under_cgi_context sub under_cgi_context { - my $mysync = shift ; + my $mysync = shift @ARG ; # Under cgi context - if ( $ENV{SERVER_SOFTWARE} ) { + if ( $ENV{SERVER_SOFTWARE} ) + { return 1 ; } # Not in cgi context return ; } -sub cgibuildheader +sub cgibuildheader { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! under_cgi_context( $mysync ) ) { return ; } my $imapsync_runs = $mysync->{cgi}->cookie( 'imapsync_runs' ) || 0 ; @@ -3653,23 +3884,35 @@ sub cgibuildheader -path => '/cgi-bin/imapsync', ) ; my $httpheader ; - if ( $mysync->{ abort } ) { + if ( $mysync->{ abort } ) + { $httpheader = $mysync->{cgi}->header( -type => 'text/plain; charset=UTF-8', - -status => '200 OK to abort syncing IMAP boxes' . ". Here is " . hostname(), + -status => '200 OK to abort syncing IMAP boxes. ' . load_message( $mysync ), ) ; - }elsif( $mysync->{ loaddelay } ) { -# https://tools.ietf.org/html/rfc2616#section-10.5.4 -# 503 Service Unavailable -# The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + } + elsif( ! $mysync->{ diskspace_available } ) + { $httpheader = $mysync->{cgi}->header( -type => 'text/plain; charset=UTF-8', - -status => '503 Service Unavailable' . ". Be back in $mysync->{ loaddelay } min. Load on " . hostname() . " is $mysync->{ loadavg }", + -status => '503 Service Unavailable. ' . "Be back later. " . 'No disk space available', ) ; - }else{ + } + elsif( $mysync->{ heavy_load_reached } ) + { + # https://tools.ietf.org/html/rfc2616#section-10.5.4 + # 503 Service Unavailable + # The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + $httpheader = $mysync->{cgi}->header( + -type => 'text/plain; charset=UTF-8', + -status => '503 Service Unavailable. ' . "Be back later. " . load_message( $mysync ), + ) ; + } + else + { $httpheader = $mysync->{cgi}->header( -type => 'text/plain; charset=UTF-8', - -status => '200 OK to sync IMAP boxes' . ". Load on " . hostname() . " is $mysync->{ loadavg }", + -status => '200 OK to sync IMAP boxes. ' . load_message( $mysync ), -cookie => $cookie, ) ; } @@ -3678,22 +3921,324 @@ sub cgibuildheader return ; } -sub cgiload +sub load_message { - # Exit on heavy load in CGI context - my $mysync = shift ; - if ( ! under_cgi_context( $mysync ) ) { return ; } - if ( $mysync->{ abort } ) { return ; } # keep going to abort since some ressources will be free soon - if ( $mysync->{ loaddelay } ) + my $mysync = shift @ARG ; + + my $message = "Load on " . hostname() . " is $mysync->{ loadavg }" ; + return $message ; +} + + +sub setexitonload +{ + my $mysync = shift @ARG ; + +# No exit on heavy load by default in normal context. +# But exit on heavy load in CGI context + + if ( under_cgi_context( $mysync ) ) { - $mysync->{nb_errors}++ ; + $mysync->{ exitonload } = defined $mysync->{ exitonload } ? $mysync->{ exitonload } : 1 ; + } + else + { + $mysync->{ exitonload } = defined $mysync->{ exitonload } ? $mysync->{ exitonload } : 0 ; + } + + return ; +} + + + +sub cgi_exit_on_heavy_load +{ + # Exit on heavy load in CGI context + my $mysync = shift @ARG ; + #if ( ! under_cgi_context( $mysync ) ) { return ; } + if ( $mysync->{ abort } ) { return ; } # keep going to abort since some ressources will be free soon + if ( $mysync->{ heavy_load_reached } ) + { + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EX_UNAVAILABLE, - "Server is on heavy load. Be back in $mysync->{ loaddelay } min. Load is $mysync->{ loadavg }\n" + "Server is on heavy load. Be back later. " . load_message( $mysync ) . "\n" ) ; } return ; +} + + +sub cgi_exit_on_no_disk_space +{ + # Exit on no disk space in CGI context + my $mysync = shift @ARG ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + if ( ! $mysync->{ diskspace_available } ) + { + $mysync->{ nb_errors }++ ; + exit_clean( $mysync, $EX_UNAVAILABLE, + "Server is in bad shape. Be back later. " . 'No disk space available' . "\n" + ) ; + } + return ; +} + + + +sub tests_heavy_load_reached +{ + note( 'Entering tests_is_heavy_load_reached()' ) ; + + like( heavy_load_reached( ), qr{^(0|1)$}xms, 'heavy_load_reached: no args => 0 or 1' ) ; + + my $mysync = { } ; + like( heavy_load_reached( $mysync ), qr{^(0|1)$}xms, 'heavy_load_reached: { } => 0 or 1' ) ; + + $mysync->{ exitonload } = 0 ; + is( 0, heavy_load_reached( $mysync ), 'heavy_load_reached: exitonload=0 => 0 ' ) ; + + note( 'Leaving tests_heavy_load_reached()' ) ; + return ; } +sub heavy_load_reached +{ + my $mysync = shift @ARG ; + + my $heavy_load_reached = 0 ; + + if ( defined( $mysync->{ exitonload } ) && ( ! $mysync->{ exitonload } ) ) + { + return 0 ; + } + + my $heavy_load_reached_by_memory = heavy_load_reached_by_memory( $mysync ) ; + my $heavy_load_reached_by_cpu = heavy_load_reached_by_cpu( $mysync ) ; + + if ( $heavy_load_reached_by_memory || $heavy_load_reached_by_cpu ) + { + $heavy_load_reached = 1 ; + } + else + { + $heavy_load_reached = 0 ; + } + + return $heavy_load_reached ; +} + + +sub tests_heavy_load_reached_by_cpu +{ + note( 'Entering tests_heavy_load_reached_by_cpu()' ) ; + + note( join( " ", loadavg( ), 'heavy_load_reached_by_cpu ', heavy_load_reached_by_cpu( ) ) ) ; + like( heavy_load_reached_by_cpu( ), qr{^(0|1)$}xms, 'heavy_load_reached_by_cpu: no args => 0 or 1' ) ; + + my $mysync = { } ; + like( heavy_load_reached_by_cpu( $mysync ), qr{^(0|1)$}xms, 'heavy_load_reached_by_cpu: { } => 0 or 1' ) ; + + note( 'Leaving tests_heavy_load_reached_by_cpu()' ) ; + return ; +} + +sub heavy_load_reached_by_cpu +{ + my $mysync = shift @ARG ; + + my $heavy_load_reached = 0 ; + + my $load_and_delay = load_per_cpu( $mysync ) ; + + if ( $load_and_delay ) + { + $heavy_load_reached = 1 ; + } + else + { + $heavy_load_reached = 0 ; + } + + return $heavy_load_reached ; +} + + +sub tests_load_per_cpu +{ + note( 'Entering tests_load_per_cpu()' ) ; + + note( join( " ", 'loadavg:', loadavg( ), 'cpu_number:', cpu_number( ), 'load_per_cpu ', load_per_cpu( ) ) ) ; + like( load_per_cpu( ), qr{^([0-9.]+)$}xms, 'tests_load_per_cpu: no args => number' ) ; + + my $mysync = { } ; + like( load_per_cpu( $mysync ), qr{^([0-9.]+)$}xms, 'tests_load_per_cpu: { } => number' ) ; + + note( 'Leaving tests_load_per_cpu()' ) ; + return ; +} + + +sub load_per_cpu +{ + my $mysync = shift @ARG ; + + return load_and_delay( 1, cpu_number( ), loadavg( ) ) ; +} + +sub tests_heavy_load_reached_by_memory +{ + note( 'Entering tests_heavy_load_reached_by_memory()' ) ; + + like( heavy_load_reached_by_memory( ), qr{^(0|1)$}xms, 'heavy_load_reached_by_memory: no args => 0 or 1' ) ; + + my $mysync = { } ; + like( heavy_load_reached_by_memory( $mysync ), qr{^(0|1)$}xms, 'heavy_load_reached_by_memory: { } => 0 or 1' ) ; + + note( 'Leaving tests_heavy_load_reached_by_memory()' ) ; + return ; +} + + +sub heavy_load_reached_by_memory +{ + my $mysync = shift @ARG ; + + my $heavy_load_reached = 0 ; + + my $heavy_load_percent_threshold = heavy_load_percent_threshold( $mysync ) ; + my $memory_consumption_all_pids_percent = memory_consumption_all_pids_percent( $mysync ) ; + + if ( $memory_consumption_all_pids_percent > $heavy_load_percent_threshold ) + { + $heavy_load_reached = 1 ; + } + else + { + $heavy_load_reached = 0 ; + } + + return $heavy_load_reached ; +} + +sub tests_heavy_load_percent_threshold +{ + note( 'Entering tests_heavy_load_percent_threshold()' ) ; + note( heavy_load_percent_threshold( ) . " (%)" ) ; + like( heavy_load_percent_threshold( ), qr{^\d+$}xms, 'heavy_load_percent_threshold: no args => integer' ) ; + my $mysync = { } ; + like( heavy_load_percent_threshold( ), qr{^\d+$}xms, 'heavy_load_percent_threshold: { } => integer' ) ; + + note( 'Leaving tests_heavy_load_percent_threshold()' ) ; + return ; +} + + +sub heavy_load_percent_threshold +{ + my $mysync = shift @ARG ; + + my $total_memory_bytes = total_ram_memory_bytes( ) || return 0 ; + my $memory_footprint_average_bytes = 250_000_000 ; + # This threshold leaves 4 memory imapsync memory footprints to the host, ie 1000 MB free for anything else. + # Small systems like 2 GB ones or even less can then present poor imapsync capacity, that's life man. + my $heavy_load_percent_threshold = max( 0, int( 100 * ( $total_memory_bytes - ( 4 * $memory_footprint_average_bytes ) ) / $total_memory_bytes ) ) ; + + return $heavy_load_percent_threshold ; +} + + +sub tests_diskspace_available +{ + note( 'Entering tests_diskspace_available()' ) ; + + is( 1, diskspace_available( ), 'tests_diskspace_available: no args => 1, could touch /var/tmp/imapsync_test_diskspace_available' ) ; + + SKIP: + { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests not done under MSWin32', 4 ) ; } + is( undef, -e '/var/tmp/imapsync_test_diskspace_available', 'tests_diskspace_available: removed /var/tmp/imapsync_test_diskspace_available' ) ; + + # with a parameter + is( 1, diskspace_available( '/var/tmp/imapsync_test_diskspace_available2' ), 'tests_diskspace_available: no args => 1, could touch /var/tmp/imapsync_test_diskspace_available2' ) ; + is( undef, -e '/var/tmp/imapsync_test_diskspace_available2', 'tests_diskspace_available: removed /var/tmp/imapsync_test_diskspace_available2' ) ; + + is( 0, diskspace_available( '/no/no/no/no' ), 'tests_diskspace_available: /no/no/no/no => undef' ) ; + } + note( 'Leaving tests_diskspace_available()' ) ; + return ; +} + + +sub diskspace_available +{ + # return 1 if diskspace available or under windows + if ( 'MSWin32' eq $OSNAME ) + { + return 1 ; + } + my $file = shift || '/var/tmp/imapsync_test_diskspace_available' ; + my $touch_test = mkdir( $file ) ; + rmdir( $file ) ; + return( $touch_test ) ; +} + + + +sub tests_tmpdir +{ + note( 'Entering tests_tmpdir()' ) ; + + is( './', tmpdir( ), 'tests_tmpdir: no args => ./' ) ; + my $mysync = { } ; + + # There is a HOME environment variable + { + local $ENV{ 'HOME' } = 'Beurk' ; + is( 'Beurk/tmp', tmpdir( $mysync ), 'tests_tmpdir: {} + $HOME => $HOME/tmp' ) ; + + undef( $ENV{ 'HOME' } ) ; + is( './', tmpdir( $mysync ), 'tests_tmpdir: {} + no $HOME => ./' ) ; + + $mysync->{ tmpdir } = 'MyBeurk/truc' ; + is( 'MyBeurk/truc', tmpdir( $mysync ), 'tests_tmpdir: {}->{tmpdir} + $HOME => {}->{tmpdir}' ) ; + } + + + note( 'Leaving tests_tmpdir()' ) ; + return ; +} + + + +sub tmpdir +{ + my $mysync = shift @ARG ; + + if ( ! defined( $mysync ) ) + { + return( './' ) ; + } + elsif ( defined( $mysync->{ tmpdir } ) ) + { + return( $mysync->{ tmpdir } ) ; + } + elsif ( defined( $ENV{ 'HOME' } ) ) + { + return( $ENV{ 'HOME' } . '/tmp' ) ; + } + else + { + return( './' ) ; + } + + + + return ; +} + + + + sub tests_set_umask { note( 'Entering tests_set_umask()' ) ; @@ -3701,7 +4246,8 @@ sub tests_set_umask my $save_umask = umask ; my $mysync = {} ; - if ( 'MSWin32' eq $OSNAME ) { + if ( 'MSWin32' eq $OSNAME ) + { is( undef, set_umask( $mysync ), "set_umask: set failure to $UMASK_PARANO on MSWin32" ) ; }else{ is( 1, set_umask( $mysync ), "set_umask: set to $UMASK_PARANO" ) ; @@ -3714,11 +4260,12 @@ sub tests_set_umask sub set_umask { - my $mysync = shift ; + my $mysync = shift @ARG ; my $previous_umask = umask_str( ) ; my $new_umask = umask_str( $UMASK_PARANO ) ; output( $mysync, "Umask set with $new_umask (was $previous_umask)\n" ) ; - if ( $new_umask eq $UMASK_PARANO ) { + if ( $new_umask eq $UMASK_PARANO ) + { return 1 ; } return ; @@ -3740,14 +4287,15 @@ sub tests_umask_str is( '0400', umask_str( '0400' ), 'umask_str: 0400 => 0400' ) ; is( '0600', umask_str( '0600' ), 'umask_str: 0600 => 0600' ) ; - SKIP: { - if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests success only for Unix', 6 ) ; } - is( '0100', umask_str( '0100' ), 'umask_str: 0100 => 0100' ) ; - is( '0001', umask_str( '0001' ), 'umask_str: 0001 => 0001' ) ; - is( '0777', umask_str( '0777' ), 'umask_str: 0777 => 0777' ) ; - is( '0777', umask_str( '00777' ), 'umask_str: 00777 => 0777' ) ; - is( '0777', umask_str( ' 777 ' ), 'umask_str: 777 => 0777' ) ; - is( "$UMASK_PARANO", umask_str( $UMASK_PARANO ), "umask_str: UMASK_PARANO $UMASK_PARANO => $UMASK_PARANO" ) ; + SKIP: + { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests success only for Unix', 6 ) ; } + is( '0100', umask_str( '0100' ), 'umask_str: 0100 => 0100' ) ; + is( '0001', umask_str( '0001' ), 'umask_str: 0001 => 0001' ) ; + is( '0777', umask_str( '0777' ), 'umask_str: 0777 => 0777' ) ; + is( '0777', umask_str( '00777' ), 'umask_str: 00777 => 0777' ) ; + is( '0777', umask_str( ' 777 ' ), 'umask_str: 777 => 0777' ) ; + is( "$UMASK_PARANO", umask_str( $UMASK_PARANO ), "umask_str: UMASK_PARANO $UMASK_PARANO => $UMASK_PARANO" ) ; } is( $save_umask_str, umask_str( $save_umask_str ), 'umask_str: restore with str' ) ; @@ -3761,9 +4309,10 @@ sub tests_umask_str sub umask_str { - my $value = shift ; + my $value = shift @ARG ; - if ( defined $value ) { + if ( defined $value ) + { umask oct( $value ) ; } my $current = umask ; @@ -3795,45 +4344,138 @@ sub tests_umask return ; } -sub buggyflagsregex + +sub make_var_array_to_a_hash { - # From /X analyse - # cut -d: -f1 Error_112_all_syncs.txt | xargs egrep -oih 'Invalid system flag [^( ]+' | sort | uniq -c | sort -g - my @buggyflagsregex = ( 's/\\\\RECEIPTCHECKED|\\\\Indexed|\\\\X-EON-HAS-ATTACHMENT|\\\\UNSEEN|\\\\ATTACHED|\\\\X-HAS-ATTACH|\\\\FORWARDED|\\\\FORWARD|\\\\X-FORWARDED|\\\\\$FORWARDED|\\\\PRIORITY|\\\\READRCPT//g' ) ; - return( @buggyflagsregex ) ; + my $mysync = shift @ARG ; + %{ $mysync->{ varh } } = split_around_equal( @{ $mysync->{ var } } ) ; + + return ; } +sub under_Net_Server_HTTP +{ + my $under_Net_Server_HTTP = $ENV{ 'NET_SERVER_SOFTWARE' } and ( $ENV{ 'NET_SERVER_SOFTWARE' } =~ /Net::Server::HTTP/ ) ; + return $under_Net_Server_HTTP ; +} + + +sub lamiral_host +{ + my $hostname = hostname() ; + if ( + ( $hostname eq 'petite' ) + or + ( $hostname eq 'ks5' ) + or + ( $hostname eq 'ks7' ) + or + ( $hostname eq 'ks8' ) + or + ( $hostname eq 'macosx' ) + or + ( $hostname eq 'i050' ) + or + ( $hostname eq 'i021' ) + ) + { + return 1 ; + } + else + { + return 0 ; + } +} + + +sub tests_cgidir +{ + note( 'Entering tests_cgidir()' ) ; + + is( undef, cgidir( ), 'tests_cgidir: no args => undef' ) ; + + SKIP: { + if ( skip_docker( ) or not lamiral_host( ) ) + { + skip( 'Tests avoided in Docker context or foreign host', 3 ) ; + } + else + { + my $mysync = { } ; + is( '/var/tmp/imapsync_cgi/385d7a4d8d428d7aa2b57c8982629e2bd67698ed', cgidir( $mysync ), 'tests_cgidir: { } => /var/tmp/imapsync_cgi/385d...' ) ; + + { + local $ENV{ NET_SERVER_SOFTWARE } = 'Net::Server::HTTP' ; + is( '.', cgidir( $mysync ), 'tests_cgidir: Net::Server::HTTP => .' ) ; + + $mysync->{ dockercontext } = 1 ; + is( '/var/tmp/imapsync_cgi/385d7a4d8d428d7aa2b57c8982629e2bd67698ed', cgidir( $mysync ), 'tests_cgidir: Net::Server::HTTP + Docker => /var/tmp/imapsync_cgi/385d...' ) ; + } ; + + } + } + + note( 'Leaving tests_cgidir()' ) ; + return ; +} + +sub cgidir +{ + my $mysync = shift @ARG ; + + if ( ! defined( $mysync ) ) { return ; } ; + + my $cgidir = shift @ARG ; + + if ( ! defined( $cgidir ) ) + { + + $mysync->{ hashfile } = $CGI_HASHFILE ; + my $hashsynclocal = hashsynclocal( $mysync ) || die "Can not get hashsynclocal. Exiting\n" ; + + if ( ! under_docker_context( $mysync ) and under_Net_Server_HTTP( ) ) + { + # under local webserver + $cgidir = q{.} ; + } + else + { + $cgidir = $CGI_TMPDIR_TOP . '/' . $hashsynclocal ; + } + } + + return( $cgidir ) ; +} + + + sub cgisetcontext { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! under_cgi_context( $mysync ) ) { return ; } output( $mysync, "Under cgi context\n" ) ; - - set_umask( $mysync ) ; # Remove all content in unsafe evaled options @{ $mysync->{ regextrans2 } } = ( ) ; @{ $mysync->{ regexflag } } = buggyflagsregex( ) ; - + @regexmess = ( ) ; @skipmess = ( ) ; @pipemess = ( ) ; - $delete2foldersonly = undef ; - $delete2foldersbutnot = undef ; - $maxlinelengthcmd = undef ; + $mysync->{ delete2foldersonly } = undef ; + $mysync->{ delete2foldersbutnot } = undef ; # Set safe default values (I hope...) - - #$mysync->{pidfile} = 'imapsync.pid' ; + #$mysync->{ pidfile } = 'imapsync.pid' ; $mysync->{ pidfilelocking } = 1 ; $mysync->{ errorsmax } = $ERRORS_MAX_CGI ; - $modulesversion = 0 ; + $mysync->{ modulesversion } = 0 ; $mysync->{ releasecheck } = defined $mysync->{ releasecheck } ? $mysync->{ releasecheck } : 1 ; - $usecache = 0 ; + $mysync->{ usecache } = 0 ; $mysync->{ showpasswords } = 0 ; $mysync->{ acc1 }->{ debugimap } = 0 ; $mysync->{ acc2 }->{ debugimap } = 0 ; @@ -3841,26 +4483,16 @@ sub cgisetcontext $mysync->{ acc1 }->{ reconnectretry } = $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; $mysync->{ acc2 }->{ reconnectretry } = $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; - $pipemesscheck = 0 ; + $mysync->{ pipemesscheck } = 0 ; - $mysync->{ hashfile } = $CGI_HASHFILE ; - my $hashsynclocal = hashsynclocal( $mysync ) || die "Can not get hashsynclocal. Exiting\n" ; - - if ( $ENV{ 'NET_SERVER_SOFTWARE' } and ( $ENV{ 'NET_SERVER_SOFTWARE' } =~ /Net::Server::HTTP/ ) ) - { - # under local webserver - $cgidir = q{.} ; - } - else - { - $cgidir = $CGI_TMPDIR_TOP . '/' . $hashsynclocal ; - } + $cgidir = cgidir( $mysync ) ; + -d $cgidir or mkpath $cgidir or die "Can not create $cgidir: $OS_ERROR\n" ; $mysync->{ tmpdir } = $cgidir ; $mysync->{ logdir } = '' ; - + chdir $cgidir or die "Can not cd to $cgidir: $OS_ERROR\n" ; - cgioutputenvcontext( $mysync ) ; + #output( $mysync, cgienvcontext( $mysync ) ) ; $mysync->{ debug } and output( $mysync, 'Current directory is ' . getcwd( ) . "\n" ) ; $mysync->{ debug } and output( $mysync, 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; $mysync->{ debug } and output( $mysync, 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; @@ -3873,36 +4505,96 @@ sub cgisetcontext # tail -f behaviour on by default $mysync->{ tail } = defined $mysync->{ tail } ? $mysync->{ tail } : 1 ; - # not sure it's for good - @useheader = qw( Message-Id Received ) ; + # + @{ $mysync->{ useheader } } = defined( $mysync->{ useheader } ) ? @{ $mysync->{ useheader } } : qw( Message-Id Received ) ; # addheader on by default $mysync->{ addheader } = defined $mysync->{ addheader } ? $mysync->{ addheader } : 1 ; - + # sync duplicates by default in cgi context $mysync->{ syncduplicates } = defined $mysync->{ syncduplicates } ? $mysync->{ syncduplicates } : 1 ; # log the logfile name by default in cgi context $mysync->{ loglogfile } = defined $mysync->{ loglogfile } ? $mysync->{ loglogfile } : 1 ; + return ; } -sub cgioutputenvcontext + + +sub tests_cgienvcontext +{ + note( 'Entering tests_cgienvcontext()' ) ; + + is( '', cgienvcontext( ), 'cgienvcontext: no args => empty' ) ; + my $mysync = { } ; + is( '', cgienvcontext( $mysync ), 'cgienvcontext: undef => empty' ) ; + + # environment SERVER_SOFTWARE alone + local $ENV{SERVER_SOFTWARE} = 'Chateau Lami' ; + is( "SERVER_SOFTWARE is Chateau Lami\n", cgienvcontext( $mysync ), 'cgienvcontext: SERVER_SOFTWARE=Chateau Lami' ) ; + + # environment REMOTE_HOST and SERVER_SOFTWARE + $mysync = { } ; + local $ENV{REMOTE_HOST} = 'Votre serviteur' ; + is( "REMOTE_HOST is Votre serviteur\nSERVER_SOFTWARE is Chateau Lami\n", cgienvcontext( $mysync ), 'cgienvcontext: SERVER_SOFTWARE + REMOTE_HOST' ) ; + + # environment REMOTE_HOST and SERVER_SOFTWARE and --var REMOTE_HOST + $mysync->{ varh }->{REMOTE_HOST} = 'Another Servant' ; + is( "REMOTE_HOST is Another Servant and Votre serviteur\nSERVER_SOFTWARE is Chateau Lami\n", cgienvcontext( $mysync ), 'cgienvcontext: SERVER_SOFTWARE + REMOTE_HOST + --var REMOTE_HOST' ) ; + + + # environment SERVER_SOFTWARE --var REMOTE_HOST only + local $ENV{REMOTE_HOST} = undef ; + $mysync = { } ; + $mysync->{ varh }->{REMOTE_HOST} = 'Another Servant' ; + is( "REMOTE_HOST is Another Servant\nSERVER_SOFTWARE is Chateau Lami\n", cgienvcontext( $mysync ), 'cgienvcontext: SERVER_SOFTWARE + --var REMOTE_HOST' ) ; + + # environment SERVER_SOFTWARE --var REMOTE_HOST only + local $ENV{REMOTE_HOST} = undef ; + local $ENV{SERVER_SOFTWARE} = undef ; + $mysync = { } ; + $mysync->{ varh }->{REMOTE_HOST} = 'Another Servant' ; + is( "REMOTE_HOST is Another Servant\n", cgienvcontext( $mysync ), 'cgienvcontext: --var REMOTE_HOST' ) ; + + note( 'Leaving tests_cgienvcontext()' ) ; + return ; +} + +sub cgienvcontext { my $mysync = shift @ARG ; - - for my $envvar ( qw( REMOTE_ADDR REMOTE_HOST HTTP_REFERER HTTP_USER_AGENT SERVER_SOFTWARE SERVER_PORT HTTP_COOKIE ) ) { - + + if ( ! defined $mysync ) { return '' ; } + + my $output = '' ; + for my $envvar ( qw( REMOTE_ADDR REMOTE_HOST HTTP_REFERER HTTP_USER_AGENT REQUEST_URI SERVER_SOFTWARE SERVER_NAME SERVER_ADDR SERVER_PORT SERVER_ADMIN HTTP_COOKIE ) ) + { + # $envval comes from the web server via an environment variable + # $varval comes from the command line parameter --val or the cgi parameter val my $envval = $ENV{ $envvar } || q{} ; - if ( $envval ) { output( $mysync, "$envvar is $envval\n" ) } ; + my $varval = $mysync->{ varh }->{ $envvar } ; + + if ( $envval and not $varval ) + { + $output .= "$envvar is $envval\n" ; + } + elsif ( not $envval and $varval ) + { + $output .= "$envvar is $varval\n" ; + } + elsif ( $envval and $varval ) + { + $output .= "$envvar is $varval and $envval\n" ; + } } - return ; + return $output ; } -sub announcelogfile +sub announcelogfile { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( $mysync->{ log } ) { @@ -3912,41 +4604,104 @@ sub announcelogfile else { myprint( "No log file because of option --nolog\n" ) ; - } + } return ; } -sub loglogfile + + + + + +sub tests_loglogfile { - my $mysync = shift ; + note( 'Entering tests_loglogfile()' ) ; + + is( undef, loglogfile( ), 'loglogfile: no args => undef' ) ; + my $mysync = { } ; + is( undef, loglogfile( $mysync ), 'loglogfile: undef => undef' ) ; + $mysync->{ loglogfile } = 1 ; + $mysync->{ log } = 1 ; + is( undef, loglogfile( $mysync ), 'loglogfile: no logfile => undef' ) ; + $mysync->{ logfile } = "logfile.txt" ; + $mysync->{ loglogfilename } = "W/tmp/tests/list_all_logs_auto.txt" ; + like( loglogfile( $mysync ), qr{logfile.txt}xms, 'loglogfile: logfile=logfile.txt => ' ) ; + note( 'Leaving tests_loglogfile()' ) ; + return ; +} + + +sub loglogfile +{ + my $mysync = shift @ARG ; if ( ! $mysync->{ loglogfile } ) { return ; } if ( ! $mysync->{ log } ) { return ; } + if ( ! $mysync->{ logfile } ) { return ; } + + my $loglogfilename = $mysync->{ loglogfilename } || return ; + + my $absolutelogfilepath = absolutelogfilepath( $mysync ) ; + + if ( condition_to_loglogfile( $mysync ) ) + { + myprint( "Writing log file name $absolutelogfilepath to $loglogfilename\n" ) ; + append_to_file( $absolutelogfilepath, $loglogfilename ) ; + } + + return $absolutelogfilepath ; +} + +sub condition_to_loglogfile +{ + my $mysync = shift @ARG ; + + if ( defined $mysync->{ cmdcgi } and scalar( @{ $mysync->{ cmdcgi } } ) ) + { + return 1 ; + } + else + { + return 0 ; + } +} + +sub absolutelogfilepath +{ + my $mysync = shift @ARG ; - my $cwd = getcwd( ) ; - my $absolutelogfilepath ; # Fixme: add case when the logfile name is already absolute - $absolutelogfilepath = "$cwd/$mysync->{ logfile }" ; - my $loglogfilename = '../list_all_logs_auto.txt' ; - myprint( "Writing log file name $absolutelogfilepath to $loglogfilename\n" ) ; - if ( open( my $fh, '>>', $loglogfilename ) ) + + my $cwd = getcwd( ) ; + my $absolutelogfilepath = "$cwd/$mysync->{ logfile }" ; + + return $absolutelogfilepath ; +} + + +sub append_to_file +{ + my $string = shift @ARG ; + my $filename = shift @ARG ; + + if ( open( my $fh, '>>', $filename ) ) { - print $fh "$absolutelogfilepath\n" ; + print $fh "$string\n" ; close $fh ; } else { - myprint( "Could not open loglogfile $loglogfilename $!\n" ) ; + myprint( "Could not append $string to file $filename $!\n" ) ; } return ; -} +} - -sub checkselectable +sub checkselectable { - my $mysync = shift ; - - if ( $mysync->{ checkselectable } ) { + my $mysync = shift @ARG ; + + if ( $mysync->{ checkselectable } ) + { my @h1_folders_wanted_selectable ; myprint( "Host1: Checking wanted folders are selectable. Use --nocheckselectable to avoid this check.\n" ) ; foreach my $folder ( @{ $mysync->{ h1_folders_wanted } } ) @@ -3962,7 +4717,7 @@ sub checkselectable } } @{ $mysync->{ h1_folders_wanted } } = @h1_folders_wanted_selectable ; - ( $mysync->{ debug } or $mysync->{ debugfolders } ) + ( $mysync->{ debug } or $mysync->{ debugfolders } ) and myprint( 'Host1: checking folders took ', timenext( $mysync ), " s\n" ) ; } else @@ -3972,14 +4727,14 @@ sub checkselectable return ; } -sub setcheckselectable +sub setcheckselectable { - my $mysync = shift ; - + my $mysync = shift @ARG ; + my $h1_folders_wanted_nb = scalar @{ $mysync->{ h1_folders_wanted } } ; # 152 because 98% of host1 accounts have less than 152 folders on /X service. - # command to get this value: - # datamash_file_op_index G_Host1_Nb_folders.txt perc:98 4 %16.1f + # command to get this value: + # datamash_file_op_index G_Host1_Nb_folders.txt perc:98 4 %16.1f if ( ! defined $mysync->{ checkselectable } ) { if ( 152 >= $h1_folders_wanted_nb ) @@ -3998,32 +4753,21 @@ sub setcheckselectable sub debugsleep { my $mysync = shift @ARG ; - if ( defined $mysync->{debugsleep} ) { + if ( defined $mysync->{debugsleep} ) + { myprint( "Info: sleeping $mysync->{debugsleep}s\n" ) ; sleep $mysync->{debugsleep} ; } return ; } -sub tests_foldersize -{ - note( 'Entering tests_foldersize()' ) ; - - is( undef, foldersize( ), 'foldersize: no args => undef' ) ; - - - #is_deeply( {}, {}, 'foldersize: a hash is a hash' ) ; - #is_deeply( [], [], 'foldersize: an array is an array' ) ; - note( 'Leaving tests_foldersize()' ) ; - return ; -} # Globals: -# $fetch_hash_set +# # -sub foldersize +sub foldersize { # size of one folder my ( $mysync, $side, $imap, $search_cmd, $abletosearch, $folder ) = @ARG ; @@ -4049,7 +4793,7 @@ sub foldersize } my $hash_ref = { } ; - my @msgs = select_msgs( $imap, undef, $search_cmd, $abletosearch, $folder ) ; + my @msgs = select_msgs( $mysync, $imap, undef, $search_cmd, $abletosearch, $folder ) ; my $nb_msgs = scalar @msgs ; my $biggest_in_folder = 0 ; @{ $hash_ref }{ @msgs } = ( undef ) if @msgs ; @@ -4072,7 +4816,7 @@ sub foldersize } else { - my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + my $fetch_hash_uids = $mysync->{ fetch_hash_set } || "1:*" ; if ( ! $imap->fetch_hash( $fetch_hash_uids, 'RFC822.SIZE', $hash_ref ) ) { my $error = "$side failure with fetch_hash: $EVAL_ERROR\n" ; errors_incr( $mysync, $error ) ; @@ -4089,15 +4833,14 @@ sub foldersize } } return( $stot, $nb_msgs, $biggest_in_folder ) ; - -} +} # The old subroutine that performed just one side at a time. # Still here for a while, until confident with sub foldersize_diff_compute() sub foldersizes { - my ( $mysync, $side, $imap, $search_cmd, $abletosearch, @folders ) = @_ ; + my ( $mysync, $side, $imap, $search_cmd, $abletosearch, @folders ) = @ARG ; my $total_size = 0 ; my $total_nb = 0 ; my $biggest_in_all = 0 ; @@ -4143,11 +4886,11 @@ sub foldersizes sub foldersize_diff_present { - my $mysync = shift ; - my $folder1 = shift ; - my $folder2 = shift ; - my $counter_str = shift ; - my $force = shift ; + my $mysync = shift @ARG ; + my $folder1 = shift @ARG ; + my $folder2 = shift @ARG ; + my $counter_str = shift @ARG ; + my $force = shift @ARG ; my $values1_str ; my $values2_str ; @@ -4213,10 +4956,10 @@ sub foldersize_diff_present sub foldersize_diff_compute { - my $mysync = shift ; - my $folder1 = shift ; - my $folder2 = shift ; - my $force = shift ; + my $mysync = shift @ARG ; + my $folder1 = shift @ARG ; + my $folder2 = shift @ARG ; + my $force = shift @ARG ; @@ -4304,8 +5047,8 @@ sub foldersize_diff_compute sub diff { - my $x = shift ; - my $y = shift ; + my $x = shift @ARG ; + my $y = shift @ARG ; $x ||= 0 ; $y ||= 0 ; @@ -4313,15 +5056,40 @@ sub diff return $x - $y ; } +sub tests_add +{ + note( 'Entering tests_add()' ) ; + + is( 0, add( ), 'tests_add: no args => 0' ) ; + is( 0, add( undef ), 'tests_add: undef => 0' ) ; + is( 0, add( 0 ), 'tests_add: 0 => 0' ) ; + is( 0, add( 0, 0 ), 'tests_add: 0 0 => 0' ) ; + is( 0, add( 0, 0, 0 ), 'tests_add: 0 0 0 => 0' ) ; + is( 1, add( 1 ), 'tests_add: 1 => 1' ) ; + is( 2, add( 1, 1 ), 'tests_add: 1 1 => 2' ) ; + is( 3, add( 1, 1, 1 ), 'tests_add: 1 1 1 => 3' ) ; + is( 2, add( 1, undef, 1 ), 'tests_add: 1 undef 1 => 2' ) ; + is( 0, add( -1, 1 ), 'tests_add: -1 1 => 0' ) ; + + is( 2.2, add( 1.1, 1.1 ), 'tests_add: 1.1 1.1 => 2.2' ) ; + + is( 100, add( (1) x 100 ), 'tests_add: 1 1 ... 1 100 times => 100 list' ) ; + my @hundred_ones = (1) x 100 ; + is( 100, add( @hundred_ones ), 'tests_add: 1 1 ... 1 100 times => 100 array' ) ; + + note( 'Leaving tests_add()' ) ; + return ; +} + + sub add { - my $x = shift ; - my $y = shift ; - - $x ||= 0 ; - $y ||= 0 ; - - return $x + $y ; + my $sum = 0 ; + foreach my $number ( @ARG ) + { + $sum += $number || 0 ; + } + return $sum ; } sub tests_checknoabletosearch @@ -4387,8 +5155,8 @@ sub checknoabletosearch sub foldersizes_diff_list { - my $mysync = shift ; - my $force = shift ; + my $mysync = shift @ARG ; + my $force = shift @ARG ; my @folders = @{ $mysync->{h1_folders_wanted} } ; my $nb_folders = scalar @folders ; @@ -4407,7 +5175,7 @@ sub foldersizes_diff_list sub foldersizes_total { - my $mysync = shift ; + my $mysync = shift @ARG ; my @folders_1 = @{ $mysync->{h1_folders_wanted} } ; my @folders_2 = @h2_folders_from_1_wanted ; @@ -4453,18 +5221,18 @@ sub foldersizes_total sub foldersizesatend_old { - my $mysync = shift ; + my $mysync = shift @ARG ; timenext( $mysync ) ; - return if ( $mysync->{imap1}->IsUnconnected( ) ) ; - return if ( $mysync->{imap2}->IsUnconnected( ) ) ; + return if ( $mysync->{ imap1 }->IsUnconnected( ) ) ; + return if ( $mysync->{ imap2 }->IsUnconnected( ) ) ; # Get all folders on host2 again since new were created - @h2_folders_all = sort $mysync->{imap2}->folders(); + @h2_folders_all = sort $mysync->{ imap2 }->folders(); for ( @h2_folders_all ) { $h2_folders_all{ $_ } = 1 ; $mysync->{h2_folders_all_UPPER}{ uc $_ } = 1 ; } ; - ( $h1_nb_msg_end, $h1_bytes_end ) = foldersizes( $mysync, 'Host1', $mysync->{imap1}, $mysync->{ search1 }, $mysync->{abletosearch1}, @{ $mysync->{h1_folders_wanted} } ) ; - ( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( $mysync, 'Host2', $mysync->{imap2}, $mysync->{ search2 }, $mysync->{abletosearch2}, @h2_folders_from_1_wanted ) ; + ( $h1_nb_msg_end, $h1_bytes_end ) = foldersizes( $mysync, 'Host1', $mysync->{ imap1 }, $mysync->{ search1 }, $mysync->{abletosearch1}, @{ $mysync->{h1_folders_wanted} } ) ; + ( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( $mysync, 'Host2', $mysync->{ imap2 }, $mysync->{ search2 }, $mysync->{abletosearch2}, @h2_folders_from_1_wanted ) ; if ( not all_defined( $h1_nb_msg_end, $h1_bytes_end, $h2_nb_msg_end, $h2_bytes_end ) ) { my $error = "Failure getting foldersizes, final differences will not be calculated\n" ; errors_incr( $mysync, $error ) ; @@ -4474,12 +5242,12 @@ sub foldersizesatend_old sub foldersizesatend { - my $mysync = shift ; + my $mysync = shift @ARG ; timenext( $mysync ) ; - return if ( $mysync->{imap1}->IsUnconnected( ) ) ; - return if ( $mysync->{imap2}->IsUnconnected( ) ) ; + return if ( $mysync->{ imap1 }->IsUnconnected( ) ) ; + return if ( $mysync->{ imap2 }->IsUnconnected( ) ) ; # Get all folders on host2 again since new were created - @h2_folders_all = sort $mysync->{imap2}->folders(); + @h2_folders_all = sort $mysync->{ imap2 }->folders(); for ( @h2_folders_all ) { $h2_folders_all{ $_ } = 1 ; $mysync->{h2_folders_all_UPPER}{ uc $_ } = 1 ; @@ -4502,11 +5270,11 @@ sub foldersizesatend sub foldersizes_at_the_beggining { - my $mysync = shift ; + my $mysync = shift @ARG ; myprint( << 'END_SIZE' ) ; -Folders sizes before the synchronization. +Folders sizes before the synchronization. It can take some time. Be patient. You can remove foldersizes listings by using "--nofoldersizes" and "--nofoldersizesatend" but then you will also lose the ETA (Estimation Time of Arrival) given after each message copy. END_SIZE @@ -4547,7 +5315,7 @@ END_SIZE sub foldersizes_at_the_beggining_old { - my $mysync = shift ; + my $mysync = shift @ARG ; myprint( << 'END_SIZE' ) ; @@ -4557,10 +5325,10 @@ but then you will also lose the ETA (Estimation Time of Arrival) given after eac END_SIZE ( $mysync->{ h1_nb_msg_start }, $mysync->{ h1_bytes_start } ) = - foldersizes( $mysync, 'Host1', $mysync->{imap1}, $mysync->{ search1 }, + foldersizes( $mysync, 'Host1', $mysync->{ imap1 }, $mysync->{ search1 }, $mysync->{abletosearch1}, @{ $mysync->{h1_folders_wanted} } ) ; ( $mysync->{ h2_nb_msg_start }, $mysync->{ h2_bytes_start } ) = - foldersizes( $mysync, 'Host2', $mysync->{imap2}, $mysync->{ search2 }, + foldersizes( $mysync, 'Host2', $mysync->{ imap2 }, $mysync->{ search2 }, $mysync->{abletosearch2}, @h2_folders_from_1_wanted ) ; if ( not all_defined( $mysync->{ h1_nb_msg_start }, @@ -4611,7 +5379,7 @@ sub tests_total_bytes_max_reached sub total_bytes_max_reached { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! defined $mysync ) { return ; } @@ -4708,7 +5476,7 @@ sub tests_mock_capability sub sig_install_toggle_sleep { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( 'MSWin32' ne $OSNAME ) { #myprint( "sig_install( $mysync, \&toggle_sleep, 'USR1' )\n" ) ; sig_install( $mysync, 'toggle_sleep', 'USR1' ) ; @@ -4720,7 +5488,7 @@ sub sig_install_toggle_sleep sub mock_capability { - my $myimap = shift ; + my $myimap = shift @ARG ; my @has_capability_value = @ARG ; my ( $has_capability_value ) = @has_capability_value ; @@ -4859,7 +5627,7 @@ sub tests_appendlimit_from_capability sub appendlimit_from_capability { - my $myimap = shift ; + my $myimap = shift @ARG ; if ( ! $myimap ) { myprint( "Warn: no imap with call to appendlimit_from_capability\n" ) ; @@ -5100,6 +5868,8 @@ sub tests_hashsynclocal is( undef, hashsynclocal( $mysync ), 'hashsynclocal: permission denied' ) ; } ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'hashsynclocal: mkpath W/tmp/tests/' ) ; + ok( (-d 'W/tmp/tests/' and chmod( 755, 'W/tmp/tests/' ) ), 'hashsynclocal: chmod 755 W/tmp/tests/' ) ; + $mysync->{ hashfile } = 'W/tmp/tests/imapsync_hash' ; ok( ! -e 'W/tmp/tests/imapsync_hash' || unlink 'W/tmp/tests/imapsync_hash', 'hashsynclocal: unlink W/tmp/tests/imapsync_hash' ) ; @@ -5114,15 +5884,17 @@ sub tests_hashsynclocal sub hashsynclocal { - my $mysync = shift ; - my $hashkey = shift ; # Optional, only there for tests + my $mysync = shift @ARG ; + my $hashkey = shift @ARG ; # Optional, only there for tests my $hashfile = $mysync->{ hashfile } ; $hashfile = createhashfileifneeded( $hashfile, $hashkey ) ; - if ( ! $hashfile ) { + if ( ! $hashfile ) + { return ; } $hashkey = firstline( $hashfile ) ; - if ( ! $hashkey ) { + if ( ! $hashkey ) + { myprint( "No hashkey!\n" ) ; return ; } @@ -5151,7 +5923,7 @@ sub tests_hashsync $mysync->{ password1 } = 'abcd' ; is( 'afa29ab8534495251ac8346a985717c54bc49c26', hashsync( $mysync, q{} ), 'hashsync: password1: abcd' ) ; - # A user reported a massive failure on /X (Thomas V. 21/04/2020 à 21:41 Subject: Error) + # A user reported a massive failure on /X (Thomas V. 21/04/2020 à 21:41 Subject: Error) # "Wide character in subroutine entry at /usr/local/lib/perl5/site_perl/Digest/HMAC.pm" # I can reproduce it now @@ -5164,29 +5936,29 @@ sub tests_hashsync # this one is not fatal is( undef, $eval = eval { 1 / 0 } , 'hashsync: 1/0 not fatal' ) ; # this one neither - $mysync->{ password1 } = 'Ö' ; - is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', $eval = eval { hashsync( $mysync, q{} ) } , 'hashsync: password1: Ö with eval' ) ; + $mysync->{ password1 } = 'Ö' ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', $eval = eval { hashsync( $mysync, q{} ) } , 'hashsync: password1: Ö with eval' ) ; - $mysync->{ password1 } = 'Ö' ; - is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hashsync( $mysync, q{} ), 'hashsync: password1: Ö without eval' ) ; + $mysync->{ password1 } = 'Ö' ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hashsync( $mysync, q{} ), 'hashsync: password1: Ö without eval' ) ; $mysync->{ password1 } = qq{\x{00D6}} ; is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', $eval = eval { hashsync( $mysync, q{} ) }, 'hashsync: password1: \x{00D6}' ) ; # - print qq{1 00D6:Ö\n} ; - print encode_utf8( qq{2 00D6:Ö\n} ) ; + print qq{1 00D6:Ö\n} ; + print encode_utf8( qq{2 00D6:Ö\n} ) ; print qq{3 00D6:\x{00D6}\n} ; print encode_utf8( qq{4 00D6:\x{00D6}\n} ) ; - print qq{5 6536:æ”¶\n} ; - print encode_utf8( qq{6 6536:æ”¶\n} ) ; + print qq{5 6536:收\n} ; + print encode_utf8( qq{6 6536:收\n} ) ; # the next one prints "Wide character in print at ./imapsync line xxxx" print qq{7 6536:\x{6536}\n} ; print encode_utf8( qq{8 6536:\x{6536}\n} ) ; - $mysync->{ password1 } = qq{æ”¶} ; - is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hashsync( $mysync, q{} ), 'hashsync: password1: æ”¶' ) ; + $mysync->{ password1 } = qq{收} ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hashsync( $mysync, q{} ), 'hashsync: password1: 收' ) ; $mysync->{ password1 } = qq{\x{6536}} ; is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', $eval = eval{ hashsync( $mysync, q{} ) }, 'hashsync: password1: \x{6536} with eval' ) ; @@ -5201,8 +5973,8 @@ sub tests_hashsync sub hashsync { - my $mysync = shift ; - my $hashkey = shift ; + my $mysync = shift @ARG ; + my $hashkey = shift @ARG ; my $mystring = join( q{}, $mysync->{ host1 } || q{}, @@ -5229,10 +6001,10 @@ sub tests_hmac_sha1_hex is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex( '', '', 'caca' ), 'hmac_sha1_hex: empty strings + caca => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; # Good - is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex( 'Ö' ), 'hmac_sha1_hex: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex( 'Ö' ), 'hmac_sha1_hex: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex( encode_utf8(qq{\x{00D6}}) ), 'hmac_sha1_hex: encode_utf8 \x{00D6} => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; # Bad - is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; + is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', hmac_sha1_hex( qq{\x{00D6}} ), 'hmac_sha1_hex: qq{\x{00D6}} => bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a' ) ; # Good @@ -5249,10 +6021,10 @@ sub tests_hmac_sha1_hex # http://unicode.scarfboy.com/?s=U%2B6536 # Good - is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( 'æ”¶' ), 'hmac_sha1_hex: æ”¶ => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( '收' ), 'hmac_sha1_hex: 收 => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( encode_utf8(qq{\x{6536}}) ), 'hmac_sha1_hex: encode_utf8 \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; # Bad - is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex( encode_utf8( 'æ”¶' ) ), 'hmac_sha1_hex: encode_utf8 æ”¶ => e82217119628ad03e659cc89671d05ea4cee7238' ) ; + is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex( encode_utf8( '收' ) ), 'hmac_sha1_hex: encode_utf8 收 => e82217119628ad03e659cc89671d05ea4cee7238' ) ; # Very very bad, perl dies... #is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( qq{\x{6536}} ), 'hmac_sha1_hex: \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; # Ok but well, bad indeed @@ -5273,10 +6045,10 @@ sub tests_hmac_sha1_hex_robust is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex_robust( '', '', 'caca' ), 'hmac_sha1_hex_robust: empty strings + caca => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; # Good - is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex_robust( 'Ö' ), 'hmac_sha1_hex_robust: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex_robust( 'Ö' ), 'hmac_sha1_hex_robust: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex_robust( encode_utf8(qq{\x{00D6}}) ), 'hmac_sha1_hex_robust: encode_utf8 \x{00D6} => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; # Bad - is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex_robust( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex_robust: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; + is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex_robust( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex_robust: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', hmac_sha1_hex_robust( qq{\x{00D6}} ), 'hmac_sha1_hex_robust: qq{\x{00D6}} => bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a' ) ; # Good @@ -5293,10 +6065,10 @@ sub tests_hmac_sha1_hex_robust # http://unicode.scarfboy.com/?s=U%2B6536 # Good - is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( 'æ”¶' ), 'hmac_sha1_hex_robust: æ”¶ => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( '收' ), 'hmac_sha1_hex_robust: 收 => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( encode_utf8(qq{\x{6536}}) ), 'hmac_sha1_hex_robust: encode_utf8 \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; # Bad - is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex_robust( encode_utf8( 'æ”¶' ) ), 'hmac_sha1_hex_robust: encode_utf8 æ”¶ => e82217119628ad03e659cc89671d05ea4cee7238' ) ; + is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex_robust( encode_utf8( '收' ) ), 'hmac_sha1_hex_robust: encode_utf8 收 => e82217119628ad03e659cc89671d05ea4cee7238' ) ; # Good is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( qq{\x{6536}} ), 'hmac_sha1_hex_robust: \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; # Good again @@ -5309,7 +6081,7 @@ sub tests_hmac_sha1_hex_robust sub hmac_sha1_hex_robust { - my $string = shift ; + my $string = shift @ARG ; my $val ; if ( defined( $val = eval{ hmac_sha1_hex( $string, @ARG ) } ) ) { @@ -5337,19 +6109,22 @@ sub tests_createhashfileifneeded sub createhashfileifneeded { - my $hashfile = shift ; - my $hashkey = shift || rand32( ) ; + my $hashfile = shift @ARG ; + my $hashkey = shift || generate_a_random_string( 32 ) ; # no name - if ( ! $hashfile ) { + if ( ! $hashfile ) + { return ; } # already there - if ( -e -r $hashfile ) { + if ( -e -r $hashfile ) + { return $hashfile ; } # not creatable - if ( ! -w dirname( $hashfile ) ) { + if ( ! -w dirname( $hashfile ) ) + { return ; } # creatable @@ -5362,43 +6137,55 @@ sub createhashfileifneeded print $FILE_HANDLE $hashkey ; close $FILE_HANDLE ; # Should be there now - if ( -e -r $hashfile ) { + if ( -e -r $hashfile ) + { return $hashfile ; } # unknown failure return ; } -sub tests_rand32 +sub tests_generate_a_random_string { - note( 'Entering tests_rand32()' ) ; + note( 'Entering tests_generate_a_random_string()' ) ; - my $string = rand32( ) ; - myprint( "$string\n" ) ; - is( 32, length( $string ), 'rand32: 32 characters long' ) ; - is( 32, length( rand32( ) ), 'rand32: 32 characters long, another one' ) ; - - note( 'Leaving tests_rand32()' ) ; - return ; + my $str_a ; + my $str_b ; + ok( $str_a = generate_a_random_string( ), 'tests_generate_a_random_string: give me a random string' ) ; + is( 64, length( $str_a ), 'tests_generate_a_random_string: 64 characters long' ) ; + ok( $str_b = generate_a_random_string( ), 'tests_generate_a_random_string: give another one' ) ; + is( 64, length( $str_b ), 'tests_generate_a_random_string: 64 characters long' ) ; + note( "$str_a $str_b" ) ; + isnt( $str_a, $str_b, 'tests_generate_a_random_string: hope they are different' ) ; + + my $str_c ; + ok( $str_c = generate_a_random_string( 11 ), 'tests_generate_a_random_string: give me a 11 characters random string' ) ; + is( 11, length( $str_c ), 'tests_generate_a_random_string: 11 characters long' ) ; + note( "$str_c" ) ; + note( 'Leaving tests_generate_a_random_string()' ) ; + return ; } -sub rand32 + +sub generate_a_random_string { + my $num = shift || 64 ; my @chars = ( "a".."z" ) ; my $string; - $string .= $chars[rand @chars] for 1..32 ; + $string .= $chars[rand @chars] for 1..$num ; return $string ; } + sub imap_id_stuff { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( not $mysync->{id} ) { return ; } ; - $mysync->{h1_imap_id} = imap_id( $mysync, $mysync->{imap1}, 'Host1' ) ; + $mysync->{h1_imap_id} = imap_id( $mysync, $mysync->{ imap1 }, 'Host1' ) ; #myprint( 'Host1: ' . $mysync->{h1_imap_id} ) ; - $mysync->{h2_imap_id} = imap_id( $mysync, $mysync->{imap2}, 'Host2' ) ; + $mysync->{h2_imap_id} = imap_id( $mysync, $mysync->{ imap2 }, 'Host2' ) ; #myprint( 'Host2: ' . $mysync->{h2_imap_id} ) ; return ; @@ -5406,7 +6193,7 @@ sub imap_id_stuff sub imap_id { - my ( $mysync, $imap, $Side ) = @_ ; + my ( $mysync, $imap, $Side ) = @ARG ; if ( not $mysync->{id} ) { return q{} ; } ; @@ -5418,13 +6205,13 @@ sub imap_id myprint( "$Side: No ID capability\n" ) ; }else{ my $id_inp = imapsync_id( $mysync, { side => lc $Side } ) ; - myprint( "\n$Side: found ID capability. Sending/receiving ID, presented in raw IMAP for now.\n" + myprint( "$Side: found ID capability. Sending/receiving ID, presented in raw IMAP for now.\n" . "In order to avoid sending/receiving ID, use option --noid\n" ) ; my $debug_before = $imap->Debug( ) ; $imap->Debug( 1 ) ; my $id_out = $imap->tag_and_run( 'ID ' . $id_inp ) ; #my $id_out = $imap->tag_and_run( 'ID NIL' ) ; - myprint( "\n" ) ; + #myprint( "\n" ) ; $imap->Debug( $debug_before ) ; #$imap_id_response = Data::Dumper->Dump( [ $id_out ], [ 'IMAP_ID' ] ) ; } @@ -5433,8 +6220,8 @@ sub imap_id sub imapsync_id { - my $mysync = shift ; - my $overhashref = shift ; + my $mysync = shift @ARG ; + my $overhashref = shift @ARG ; # See http://tools.ietf.org/html/rfc2971.html my $imapsync_id = { } ; @@ -5446,7 +6233,7 @@ sub imapsync_id vendor => 'Gilles LAMIRAL', 'support-url' => 'https://imapsync.lamiral.info/', # Example of date-time: 19-Sep-2015 08:56:07 - date => date_from_rcs( q{$Date: 2022/01/12 21:28:37 $ } ), + date => date_from_rcs( q{$Date: 2025/09/23 16:55:17 $ } ), } ; my $imapsync_id_github = { @@ -5455,7 +6242,7 @@ sub imapsync_id os => $OSNAME, vendor => 'github', 'support-url' => 'https://github.com/imapsync/imapsync', - date => date_from_rcs( q{$Date: 2022/01/12 21:28:37 $ } ), + date => date_from_rcs( q{$Date: 2025/09/23 16:55:17 $ } ), } ; $imapsync_id = $imapsync_id_lamiral ; @@ -5489,7 +6276,7 @@ sub tests_imapsync_id sub format_for_imap_arg { - my $ref = shift ; + my $ref = shift @ARG ; my $string = q{} ; my %terms = %{ $ref } ; @@ -5521,9 +6308,9 @@ sub tests_format_for_imap_arg return ; } -sub quota +sub quota { - my ( $mysync, $imap, $side ) = @_ ; + my ( $mysync, $imap, $side ) = @ARG ; my %side = ( h1 => 'Host1', @@ -5574,19 +6361,25 @@ sub tests_quota_extract_storage_limit_in_bytes '* QUOTA "Messages quota" (MESSAGE 2 100000)', '5 OK Getquotaroot completed.' ] ; - ok( $NUMBER_104_857_600 * $KIBI == quota_extract_storage_limit_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_limit_in_bytes ') ; - + is( $NUMBER_104_857_600 * $KIBI, quota_extract_storage_limit_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_limit_in_bytes (STORAGE 1 104857600) => 104857600 * 1024') ; + $imap_output = [ + '* QUOTAROOT "INBOX" "Storage quota" "Messages quota"', + 'QUOTA "user-defined quota (konsoleH)" (STORAGE 988 48829 MESSAGE 20 20)', + '5 OK Getquotaroot completed.' + ] ; + is( 48829 * 1024, quota_extract_storage_limit_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_limit_in_bytes (STORAGE 988 48829 MESSAGE 20 20) => 48829 * 1024') ; + note( 'Leaving tests_quota_extract_storage_limit_in_bytes()' ) ; return ; } sub quota_extract_storage_limit_in_bytes { - my $mysync = shift ; - my $imap_output = shift ; + my $mysync = shift @ARG ; + my $imap_output = shift @ARG ; my $limit_kb ; - $limit_kb = ( map { /.*\(\s*STORAGE\s+\d+\s+(\d+)\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; + $limit_kb = ( map { /STORAGE\s+\d+\s+(\d+)/x ? $1 : () } @{ $imap_output } )[0] ; $limit_kb ||= 0 ; $mysync->{ debug } and myprint( "storage_limit_kb = $limit_kb\n" ) ; return( $KIBI * $limit_kb ) ; @@ -5604,19 +6397,27 @@ sub tests_quota_extract_storage_current_in_bytes '* QUOTA "Messages quota" (MESSAGE 2 100000)', '5 OK Getquotaroot completed.' ] ; - ok( 1*$KIBI == quota_extract_storage_current_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_current_in_bytes: 1 => 1024 ') ; + is( 1*$KIBI, quota_extract_storage_current_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_current_in_bytes: 1 => 1024 ') ; - note( 'Leaving tests_quota_extract_storage_current_in_bytes()' ) ; + $imap_output = [ + '* QUOTAROOT "INBOX" "Storage quota" "Messages quota"', + 'QUOTA "user-defined quota (konsoleH)" (STORAGE 988 48829 MESSAGE 20 20)', + '5 OK Getquotaroot completed.' + ] ; + is( 988 * 1024, quota_extract_storage_current_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_current_in_bytes (STORAGE 988 48829 MESSAGE 20 20) => 988 * 1024') ; + + + note( 'Leaving tests_quota_extract_storage_current_in_bytes()' ) ; return ; } sub quota_extract_storage_current_in_bytes { - my $mysync = shift ; - my $imap_output = shift ; + my $mysync = shift @ARG ; + my $imap_output = shift @ARG ; my $current_kb ; - $current_kb = ( map { /.*\(\s*STORAGE\s+(\d+)\s+\d+\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; + $current_kb = ( map { /STORAGE\s+(\d+)\s+\d+/x ? $1 : () } @{ $imap_output } )[0] ; $current_kb ||= 0 ; $mysync->{ debug } and myprint( "storage_current_kb = $current_kb\n" ) ; return( $KIBI * $current_kb ) ; @@ -5624,9 +6425,20 @@ sub quota_extract_storage_current_in_bytes } + +sub make_f1f2_array_to_a_hash +{ + my $mysync = shift @ARG ; + %{ $mysync->{ f1f2h } } = split_around_equal( @{ $mysync->{ f1f2 } } ) ; + + return ; +} + + + sub automap { - my ( $mysync ) = @_ ; + my ( $mysync ) = @ARG ; if ( $mysync->{automap} ) { myprint( "Turned on automapping folders ( use --noautomap to turn off automapping )\n" ) ; @@ -5635,8 +6447,8 @@ sub automap return ; } - $mysync->{h1_special} = special_from_folders_hash( $mysync, $mysync->{imap1}, 'Host1' ) ; - $mysync->{h2_special} = special_from_folders_hash( $mysync, $mysync->{imap2}, 'Host2' ) ; + $mysync->{h1_special} = special_from_folders_hash( $mysync, $mysync->{ imap1 }, 'Host1' ) ; + $mysync->{h2_special} = special_from_folders_hash( $mysync, $mysync->{ imap2 }, 'Host2' ) ; build_possible_special( $mysync ) ; build_guess_special( $mysync ) ; @@ -5650,7 +6462,7 @@ sub automap sub build_guess_special { - my ( $mysync ) = shift ; + my ( $mysync ) = shift @ARG ; foreach my $h1_fold ( sort keys %{ $mysync->{h1_folders_all} } ) { my $special = guess_special( $h1_fold, $mysync->{possible_special}, $mysync->{h1_prefix} ) ; @@ -5681,7 +6493,7 @@ sub build_guess_special sub guess_special { - my( $folder, $possible_special_ref, $prefix ) = @_ ; + my( $folder, $possible_special_ref, $prefix ) = @ARG ; my $folder_no_prefix = $folder ; $folder_no_prefix =~ s/\Q${prefix}\E//xms ; @@ -5710,7 +6522,7 @@ sub tests_guess_special sub build_automap { - my $mysync = shift ; + my $mysync = shift @ARG ; $mysync->{ debug } and myprint( "Entering build_automap\n" ) ; foreach my $h1_fold ( @{ $mysync->{h1_folders_wanted} } ) { my $h2_fold ; @@ -5762,27 +6574,28 @@ sub build_automap # because it works well without sub build_possible_special { - my $mysync = shift ; + my $mysync = shift @ARG ; my $possible_special = { } ; # All|Archive|Drafts|Flagged|Junk|Sent|Trash $possible_special->{'\All'} = [ 'All', 'All Messages', '&BBIEQQQ1-' ] ; $possible_special->{'\Archive'} = [ 'Archive', 'Archives', '&BBAEQARFBDgEMg-' ] ; - $possible_special->{'\Drafts'} = [ 'Drafts', 'DRAFTS', '&BCcENQRABD0EPgQyBDgEOgQ4-', 'Szkice', 'Wersje robocze' ] ; + $possible_special->{'\Drafts'} = [ 'Drafts', 'DRAFTS', '&BCcENQRABD0EPgQyBDgEOgQ4-', 'Szkice', 'Wersje robocze', 'Entw&APw-rfe' ] ; $possible_special->{'\Flagged'} = [ 'Flagged', 'Starred', '&BB8EPgQ8BDUERwQ1BD0EPQRLBDU-' ] ; $possible_special->{'\Junk'} = [ 'Junk', 'junk', 'Spam', 'SPAM', '&BCEEPwQwBDw-', 'Potwierdzony spam', 'Wiadomo&AVs-ci-&AVs-mieci', 'Junk E-Mail', 'Junk Email'] ; $possible_special->{'\Sent'} = [ 'Sent', 'Sent Messages', 'Sent Items', - 'Gesendete Elemente', 'Gesendete Objekte', + 'Gesendete Elemente', 'Gesendete Objekte', 'Gesendet', '&AMk-l&AOk-ments envoy&AOk-s', 'E&AwE-le&AwE-ments envoye&AwE-s', 'Envoy&AOk-', 'Objets envoy&AOk-s', - 'Elementos enviados', + 'Elementos enviados', 'Posta inviata', '&kAFP4W4IMH8wojCkMMYw4A-', '&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-', 'Elementy wys&AUI-ane'] ; $possible_special->{'\Trash'} = [ 'Trash', 'TRASH', '&BCMENAQwBDsENQQ9BD0ESwQ1-', '&BBoEPgRABDcEOAQ9BDA-', 'Kosz', + 'Papierkorb', 'Deleted Items', 'Deleted Messages' ] ; @@ -5819,7 +6632,7 @@ sub tests_special_from_folders_hash sub special_from_folders_hash { - my ( $mysync, $imap, $side ) = @_ ; + my ( $mysync, $imap, $side ) = @ARG ; my %special = ( ) ; if ( ! defined $imap ) { return ; } @@ -5892,18 +6705,18 @@ sub tests_comment_of_error_type note( 'Entering tests_comment_of_error_type()' ) ; is( undef, comment_of_error_type( ), 'comment_of_error_type: no args => undef' ) ; - + my $mysync = { } ; is( undef, comment_of_error_type( $mysync ), 'comment_of_error_type: undef => undef' ) ; - + is( "", comment_of_error_type( $mysync, '' ), 'comment_of_error_type: "" => ""' ) ; is( "", comment_of_error_type( $mysync, 'blabla' ), 'comment_of_error_type: blabla => ""' ) ; - + is( "", comment_of_error_type( $mysync, 'ERR_UNCLASSIFIED' ), 'comment_of_error_type: ERR_UNCLASSIFIED => ""' ) ; - + like( comment_of_error_type( $mysync, 'ERR_OVERQUOTA' ), qr{100% full}, 'comment_of_error_type: ERR_OVERQUOTA => matches 100% full' ) ; - + note( 'Leaving tests_comment_of_error_type()' ) ; return ; @@ -5913,12 +6726,12 @@ sub comment_of_error_type { my $mysync = shift @ARG ; my $error_type = shift @ARG ; - + if ( ! defined $mysync ) { return ; } if ( ! defined $error_type ) { return ; } - + my $comment ; - + if ( exists( $COMMENT_OF_ERR_TYPE{ $error_type } ) ) { $comment = $COMMENT_OF_ERR_TYPE{ $error_type }->( $mysync ) ; @@ -5992,13 +6805,18 @@ sub tests_error_type error_type( 'could not append ( Subject:[For Your Consideration], Date:["29-Nov-2016 03:21:10 -0800"], Size:[5505], Flags:[\Seen] ) to folder INBOX: 275 NO Message refused because it contains a virus' ), 'error_type: could not append ... virus => ERR_APPEND_VIRUS' ) ; - - + + is( 'ERR_FLAGS', error_type( 'Host2: flags msg INBOX/957910 could not add flags [PasGlop \PasGlopRe]: 33 NO Error in IMAP command received by server.' ), 'error_type: could not add flags => ERR_FLAGS' ) ; - + + is( 'ERR_SEARCH', + error_type( 'BAD Error in IMAP command UID SEARCH: Unknown argument KAKA' ), + 'error_type: SEARCH: Unknown argument => ERR_SEARCH' + ) ; + note( 'Leaving tests_error_type()' ) ; return ; @@ -6010,8 +6828,8 @@ sub tests_error_type # with just a hash of error regexes as keys and types as values. sub error_type -{ - my $error = shift ; +{ + my $error = shift @ARG ; if ( ! defined $error ) { return 'ERR_NOTHING_REPORTED' ; } if ( ! $error ) { return 'ERR_NOTHING_REPORTED' ; } @@ -6054,6 +6872,9 @@ sub error_type # could not add flags if ( $error =~ m{could not add flags} ) { return 'ERR_FLAGS' ; } ; + # could not search + if ( $error =~ m{BAD .* SEARCH: Unknown argument} ) { return 'ERR_SEARCH' ; } ; + # Could not create folder .*Invalid mailbox name if ( $error =~ m{Could not create folder} ) { return 'ERR_CREATE' ; } ; @@ -6071,10 +6892,12 @@ sub error_type if ( $error =~ m{can not open imap connection on host1} ) { return 'ERR_CONNECTION_FAILURE_HOST1' ; } ; if ( $error =~ m{can not open imap connection on host2} ) { return 'ERR_CONNECTION_FAILURE_HOST2' ; } ; + + # Default is ERR_UNCLASSIFIED return 'ERR_UNCLASSIFIED' ; -} +} sub tests_errorclassify { @@ -6139,7 +6962,7 @@ sub tests_most_common_error sub most_common_error { - my $errors_counted_ref = shift ; + my $errors_counted_ref = shift @ARG ; if ( ! defined $errors_counted_ref ) { return 'ERR_NOTHING_REPORTED' ; } @@ -6237,25 +7060,25 @@ sub errorsdump sub errors_listing { - my $mysync = shift ; + my $mysync = shift @ARG ; $mysync->{ most_common_error } = errorsanalyse( errors_log( $mysync ) ) ; my $errors_listing = '' ; - + if ( $mysync->{ errorsdump } ) { $errors_listing = join( '', - "++++ Listing $mysync->{nb_errors} errors encountered during the sync ( avoid this listing with --noerrorsdump ).\n", + "++++ Listing $mysync->{ nb_errors } errors encountered during the sync ( avoid this listing with --noerrorsdump ).\n", errorsdump( errors_log( $mysync ) ), ) ; } - + $errors_listing .= join( '', "The most frequent error is $mysync->{ most_common_error }. ", comment_of_error_type( $mysync, $mysync->{ most_common_error } ), "\n", ) ; - + return $errors_listing ; } @@ -6270,21 +7093,16 @@ sub errors_incr myprint( @error ) ; } + # Why is it here? Looks like a safety "in case" $mysync->{ errorsmax } ||= $ERRORS_MAX ; - - + if ( $mysync->{ nb_errors } >= $mysync->{ errorsmax } ) { myprint( errorsmax_msg( $mysync ) ) ; - myprint( errors_listing( $mysync ) ) ; - - if ( $mysync->{ errorsdump } ) - { - # again since errorsdump( ) can be very verbose and masquerade previous warning - myprint( errorsmax_msg( $mysync ) ) ; - } - my $exit_value = exit_value( $mysync, $mysync->{ most_common_error } ) ; - exit_clean( $mysync, $exit_value ) ; + do_and_print_stats( $mysync ) ; + # again since do_and_print_stats() is verbose + myprint( errorsmax_msg( $mysync ) ) ; + exit_most_errors( $mysync ) ; } return ; } @@ -6307,7 +7125,7 @@ sub tests_live_result { note( 'Entering tests_live_result()' ) ; - my $nb_errors = shift ; + my $nb_errors = shift @ARG ; if ( $nb_errors ) { myprint( "Live tests failed with $nb_errors errors\n" ) ; } else { @@ -6320,21 +7138,21 @@ sub tests_live_result sub size_filtered_flag { - my $mysync = shift ; - my $h1_size = shift ; + my $mysync = shift @ARG ; + my $h1_size = shift @ARG ; if ( defined $mysync->{ maxsize } and $h1_size >= $mysync->{ maxsize } ) { return( 1 ) ; } - if ( defined $minsize and $h1_size <= $minsize ) { + if ( defined $mysync->{ minsize } and $h1_size <= $mysync->{ minsize } ) { return( 1 ) ; } return( 0 ) ; } -sub sync_flags_fir +sub sync_flags_fir { - my ( $mysync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) = @_ ; + my ( $mysync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $h1_fir_ref, $h2_fir_ref ) = @ARG ; if ( not defined $h1_msg ) { return } ; if ( not defined $h2_msg ) { return } ; @@ -6346,44 +7164,42 @@ sub sync_flags_fir my $h1_flags = $h1_fir_ref->{ $h1_msg }->{ 'FLAGS' } || q{} ; my $h2_flags = $h2_fir_ref->{ $h2_msg }->{ 'FLAGS' } || q{} ; - sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ; + sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags ) ; return ; -} +} -sub sync_flags_after_copy +sub syncflagsaftercopy { # Activated with option --syncflagsaftercopy - my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $permanentflags2 ) = @_ ; + my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg ) = @ARG ; - if ( my @h2_flags = $mysync->{imap2}->flags( $h2_msg ) ) { + if ( my @h2_flags = $mysync->{ imap2 }->flags( $h2_msg ) ) + { my $h2_flags = "@h2_flags" ; - ( $mysync->{ debug } or $debugflags ) and myprint( "Host2: msg $h2_fold/$h2_msg flags before sync flags after copy ( $h2_flags )\n" ) ; - sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ; - }else{ - myprint( "Host2: msg $h2_fold/$h2_msg could not get its flags for sync flags after copy\n" ) ; + ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host2: msg $h2_fold/$h2_msg flags before sync flags after copy ( $h2_flags )\n" ) ; + sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags ) ; + } + else + { + myprint( "Host2: msg $h2_fold/$h2_msg : could not get its flags to sync them after the copy\n" ) ; } return ; } -# Globals -# $debug -# $debugflags -# $permanentflags2 - -sub sync_flags +sub sync_flags { - my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) = @_ ; + my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags ) = @ARG ; - ( $mysync->{ debug } or $debugflags ) and + ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host1: flags init msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $h2_fold/$h2_msg flags( $h2_flags )\n" ) ; - $h1_flags = flags_for_host2( $mysync, $h1_flags, $permanentflags2 ) ; + $h1_flags = flags_for_host2( $mysync, $h1_flags, $mysync->{ permanentflags2 } ) ; $h2_flags = flagscase( $h2_flags ) ; - ( $mysync->{ debug } or $debugflags ) and + ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host1: flags filt msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $h2_fold/$h2_msg flags( $h2_flags )\n" ) ; @@ -6392,7 +7208,7 @@ sub sync_flags my @h2_flags = sort split(q{ }, $h2_flags ); my $diff = compare_lists( \@h1_flags, \@h2_flags ); - $diff and ( $mysync->{ debug } or $debugflags ) + $diff and ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host2: flags msg $h2_fold/$h2_msg replacing h2 flags( $h2_flags ) with h1 flags( $h1_flags )\n" ) ; # This sets flags exactly. So flags can be removed with this. @@ -6400,20 +7216,20 @@ sub sync_flags # to be removed on host2. Just add flags is not what # we need most of the time, so no + like in "+FLAGS.SILENT". - if ( not $mysync->{dry} and $diff and not $mysync->{imap2}->store( $h2_msg, "FLAGS.SILENT (@h1_flags)" ) ) { + if ( not $mysync->{ dry } and $diff and not $mysync->{ imap2 }->store( $h2_msg, "FLAGS.SILENT (@h1_flags)" ) ) { my $error_msg = join q{}, "Host2: flags msg $h2_fold/$h2_msg could not add flags [@h1_flags]: ", - $mysync->{imap2}->LastError || q{}, "\n" ; + $mysync->{ imap2 }->LastError || q{}, "\n" ; errors_incr( $mysync, $error_msg ) ; } return ; -} +} sub _filter { - my $mysync = shift ; + my $mysync = shift @ARG ; my $str = shift or return q{} ; my $sz = $SIZE_MAX_STR ; my $len = length $str ; @@ -6428,11 +7244,11 @@ sub _filter -sub lost_connection +sub lost_connection { - my( $mysync, $imap, $error_message ) = @_; + my( $mysync, $imap, $error_message ) = @ARG; if ( $imap->IsUnconnected( ) ) { - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; my $lcomm = $imap->LastIMAPCommand || q{} ; my $einfo = imap_last_error( $imap ) ; @@ -6447,11 +7263,11 @@ sub lost_connection else{ return( 0 ) ; } -} +} sub imap_last_error { - my $imap = shift ; + my $imap = shift @ARG ; my $einfo = $imap->LastError || @{$imap->History}[$LAST] || q{} ; chomp( $einfo ) ; return( $einfo ) ; @@ -6491,7 +7307,7 @@ sub tests_max sub max { - my @list = @_ ; + my @list = @ARG ; return( undef ) if ( 0 == scalar @list ) ; my( @numbers, @notnumbers ) ; @@ -6550,7 +7366,7 @@ sub tests_is_number sub is_number { - my $item = shift ; + my $item = shift @ARG ; if ( ! defined $item ) { return ; } @@ -6593,7 +7409,7 @@ sub tests_min sub min { - my @list = @_ ; + my @list = @ARG ; return( undef ) if ( 0 == scalar @list ) ; my( @numbers, @notnumbers ) ; @@ -6620,7 +7436,7 @@ sub min sub check_lib_version { - my $mysync = shift ; + my $mysync = shift @ARG ; $mysync->{ debug } and myprint( "IMAPClient $Mail::IMAPClient::VERSION\n" ) ; if ( '2.2.9' eq $Mail::IMAPClient::VERSION ) { myprint( "imapsync no longer supports Mail::IMAPClient 2.2.9, upgrade it\n" ) ; @@ -6636,7 +7452,7 @@ sub check_lib_version sub module_version_str { - my( $module_name, $module_version ) = @_ ; + my( $module_name, $module_version ) = @ARG ; my $str = mysprintf( "%-20s %s\n", $module_name, $module_version ) ; return( $str ) ; } @@ -6651,10 +7467,10 @@ sub modulesversion 'CGI' => sub { $CGI::VERSION }, 'Compress::Zlib' => sub { $Compress::Zlib::VERSION }, 'Crypt::OpenSSL::RSA' => sub { $Crypt::OpenSSL::RSA::VERSION }, - 'Data::Uniqid' => sub { $Data::Uniqid::VERSION }, 'Digest::HMAC_MD5' => sub { $Digest::HMAC_MD5::VERSION }, 'Digest::HMAC_SHA1' => sub { $Digest::HMAC_SHA1::VERSION }, - 'Digest::MD5' => sub { $Digest::MD5::VERSION }, + 'Digest::MD5' => sub { $Digest::MD5::VERSION }, + 'Digest::SHA' => sub { $Digest::SHA::VERSION }, 'Encode' => sub { $Encode::VERSION }, 'Encode::IMAPUTF7' => sub { $Encode::IMAPUTF7::VERSION }, 'File::Copy::Recursive' => sub { $File::Copy::Recursive::VERSION }, @@ -6678,7 +7494,6 @@ sub modulesversion 'Test::MockObject' => sub { $Test::MockObject::VERSION }, 'Time::HiRes' => sub { $Time::HiRes::VERSION }, 'Unicode::String' => sub { $Unicode::String::VERSION }, - 'URI::Escape' => sub { $URI::Escape::VERSION }, #'Lalala' => sub { $Lalala::VERSION }, ) ; @@ -6790,7 +7605,7 @@ sub get_stdin_masked sub ask_for_password_new { - my $prompt = shift ; + my $prompt = shift @ARG ; my $password = get_stdin_masked( $prompt ) ; return $password ; } @@ -6799,7 +7614,7 @@ sub ask_for_password_new sub ask_for_password { - my $prompt = shift ; + my $prompt = shift @ARG ; myprint( $prompt ) ; Term::ReadKey::ReadMode( 2 ) ; ## no critic (InputOutput::ProhibitExplicitStdin) @@ -6812,16 +7627,20 @@ sub ask_for_password # Have to refactor get_password1() get_password2() # to have only get_password() and two calls -sub get_password1 +sub get_password1 { - my $mysync = shift ; + my $mysync = shift @ARG ; - $mysync->{ password1 } + ! $mysync->{ host1 } + || ! $mysync->{ user1 } + || $mysync->{ password1 } || $mysync->{ passfile1 } || 'PREAUTH' eq $mysync->{ acc1 }->{ authmech } || 'EXTERNAL' eq $mysync->{ acc1 }->{ authmech } - || $ENV{IMAPSYNC_PASSWORD1} + || $mysync->{ acc1 }->{ oauthaccesstoken } + || $mysync->{ acc1 }->{ oauthdirect } + || $ENV{ IMAPSYNC_PASSWORD1 } || do { myprint( << 'FIN_PASSFILE' ) ; @@ -6834,36 +7653,43 @@ FIN_PASSFILE my $user = $mysync->{ acc1 }->{ authuser } || $mysync->{ user1 } ; my $host = $mysync->{ host1 } ; my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; - $mysync->{password1} = ask_for_password( $prompt ) ; + $mysync->{ password1 } = ask_for_password( $prompt ) ; } ; - if ( defined $mysync->{ passfile1 } ) { - if ( ! -e -r $mysync->{ passfile1 } ) { + if ( defined $mysync->{ passfile1 } ) + { + if ( ! -e -r $mysync->{ passfile1 } ) + { myprint( "Failure: file from parameter --passfile1 $mysync->{ passfile1 } does not exist or is not readable\n" ) ; - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EX_NOINPUT ) ; } # passfile1 readable - $mysync->{password1} = firstline ( $mysync->{ passfile1 } ) ; + $mysync->{ password1 } = firstline ( $mysync->{ passfile1 } ) ; return ; } - if ( $ENV{IMAPSYNC_PASSWORD1} ) { - $mysync->{password1} = $ENV{IMAPSYNC_PASSWORD1} ; + if ( $ENV{ IMAPSYNC_PASSWORD1 } ) + { + $mysync->{ password1 } = $ENV{ IMAPSYNC_PASSWORD1 } ; return ; } return ; -} +} -sub get_password2 +sub get_password2 { - my $mysync = shift ; + my $mysync = shift @ARG ; - $mysync->{password2} + ! $mysync->{ host2 } + || ! $mysync->{ user2 } + || $mysync->{ password2 } || $mysync->{ passfile2 } || 'PREAUTH' eq $mysync->{ acc2 }->{ authmech } || 'EXTERNAL' eq $mysync->{ acc2 }->{ authmech } - || $ENV{IMAPSYNC_PASSWORD2} + || $mysync->{ acc2 }->{ oauthaccesstoken } + || $mysync->{ acc2 }->{ oauthdirect } + || $ENV{ IMAPSYNC_PASSWORD2 } || do { myprint( << 'FIN_PASSFILE' ) ; @@ -6876,26 +7702,29 @@ FIN_PASSFILE my $user = $mysync->{ acc2 }->{ authuser } || $mysync->{ user2 } ; my $host = $mysync->{ host2 } ; my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; - $mysync->{password2} = ask_for_password( $prompt ) ; + $mysync->{ password2 } = ask_for_password( $prompt ) ; } ; - if ( defined $mysync->{ passfile2 } ) { - if ( ! -e -r $mysync->{ passfile2 } ) { + if ( defined $mysync->{ passfile2 } ) + { + if ( ! -e -r $mysync->{ passfile2 } ) + { myprint( "Failure: file from parameter --passfile2 $mysync->{ passfile2 } does not exist or is not readable\n" ) ; - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EX_NOINPUT ) ; } # passfile2 readable - $mysync->{password2} = firstline ( $mysync->{ passfile2 } ) ; + $mysync->{ password2 } = firstline ( $mysync->{ passfile2 } ) ; return ; } - if ( $ENV{IMAPSYNC_PASSWORD2} ) { - $mysync->{password2} = $ENV{IMAPSYNC_PASSWORD2} ; + if ( $ENV{ IMAPSYNC_PASSWORD2 } ) + { + $mysync->{ password2 } = $ENV{ IMAPSYNC_PASSWORD2 } ; return ; } return ; -} +} @@ -6903,38 +7732,64 @@ FIN_PASSFILE sub remove_tmp_files { my $mysync = shift or return ; - $mysync->{pidfile} or return ; + $mysync->{ pidfile } or return ; - if ( -e $mysync->{pidfile} ) { - myprint( "Removing pidfile $mysync->{pidfile}\n" ) ; - unlink $mysync->{pidfile} ; - } - if ( -e $mysync->{abortfile} ) { - myprint( "Removing pidfile $mysync->{abortfile}\n" ) ; - unlink $mysync->{abortfile} ; - } + remove_pidfile( $mysync ) ; + remove_abortfile( $mysync ) ; + return ; } -sub cleanup_before_exit + +sub remove_pidfile { - my $mysync = shift ; + my $mysync = shift or return ; + + $mysync->{ pidfile } or return ; + if ( -e $mysync->{ pidfile } ) + { + myprint( "Removing pidfile $mysync->{ pidfile }\n" ) ; + unlink $mysync->{ pidfile } ; + } + + return ; +} + +sub remove_abortfile +{ + my $mysync = shift or return ; + + $mysync->{ abortfile } or return ; + + if ( -e $mysync->{ abortfile } ) + { + myprint( "Removing pidfile $mysync->{ abortfile }\n" ) ; + unlink $mysync->{ abortfile } ; + } + return ; +} + + +sub cleanup_before_exit +{ + my $mysync = shift @ARG ; + remove_tmp_files( $mysync ) ; - - if ( $mysync->{imap1} and $mysync->{imap1}->IsConnected() ) + + if ( $mysync->{ imap1 } and $mysync->{ imap1 }->IsConnected() ) { myprint( "Disconnecting from host1 $mysync->{ host1 } user1 $mysync->{ user1 }\n" ) ; - $mysync->{imap1}->logout( ) ; + $mysync->{ imap1 }->logout( ) ; } - - if ( $mysync->{imap2} and $mysync->{imap2}->IsConnected() ) + + if ( $mysync->{ imap2 } and $mysync->{ imap2 }->IsConnected() ) { myprint( "Disconnecting from host2 $mysync->{ host2 } user2 $mysync->{ user2 }\n" ) ; - $mysync->{imap2}->logout( ) ; + $mysync->{ imap2 }->logout( ) ; } - - if ( $mysync->{log} ) { + + if ( $mysync->{ log } ) { myprint( "Log file is $mysync->{logfile} ( to change it, use --logfile filepath ; or use --nolog to turn off logging )\n" ) ; } else @@ -6942,13 +7797,13 @@ sub cleanup_before_exit myprint( "No log file because of option --nolog\n" ) ; } - if ( $mysync->{log} and $mysync->{logfile_handle} ) { + if ( $mysync->{ log } ) { #print( "Closing $mysync->{ logfile }\n" ) ; - teefinish( $mysync ) ; + teefinish( $mysync->{ tee } ) ; } - + $IO::Socket::SSL::DEBUG = 1 ; return ; -} +} sub tests_exit_value @@ -6956,17 +7811,18 @@ sub tests_exit_value note( 'Entering tests_exit_value()' ) ; is( $EXIT_CATCH_ALL, exit_value( ), 'exit_value: no args => EXIT_CATCH_ALL' ) ; - + my $mysync = { } ; is( $EXIT_CATCH_ALL, exit_value( $mysync ), 'exit_value: undef => EXIT_CATCH_ALL' ) ; - + is( $EXIT_CATCH_ALL, exit_value( $mysync, 'Blabla_unknown' ), 'exit_value: Blabla => EXIT_CATCH_ALL' ) ; is( $EXIT_CATCH_ALL, exit_value( $mysync, '' ), 'exit_value: empty => EXIT_CATCH_ALL' ) ; - - + is( $EXIT_CATCH_ALL, exit_value( $mysync, '' ), 'exit_value: undef => EXIT_CATCH_ALL' ) ; + + is( $EXIT_OVERQUOTA, exit_value( $mysync, 'ERR_OVERQUOTA' ), 'exit_value: ERR_OVERQUOTA => EXIT_OVERQUOTA' ) ; is( $EXIT_TRANSFER_EXCEEDED, exit_value( $mysync, 'ERR_TRANSFER_EXCEEDED' ), 'exit_value: ERR_TRANSFER_EXCEEDED => EXIT_TRANSFER_EXCEEDED' ) ; - + note( 'Leaving tests_exit_value()' ) ; return ; } @@ -6975,10 +7831,12 @@ sub exit_value { my $mysync = shift @ARG ; my $most_common_error = shift @ARG ; - + + if ( ! defined $mysync ) { return $EXIT_CATCH_ALL ; } if ( ! defined $most_common_error ) { return $EXIT_CATCH_ALL ; } + if ( ! $most_common_error ) { return $EXIT_CATCH_ALL ; } my $exit_value = $EXIT_VALUE_OF_ERR_TYPE{ $most_common_error } || $EXIT_CATCH_ALL ; - + return $exit_value ; } @@ -6988,13 +7846,20 @@ sub exit_most_errors { my $mysync = shift @ARG ; - myprint( errors_listing( $mysync ) ) ; - my $exit_value = exit_value( $mysync, $mysync->{ most_common_error } ) ; - exit_clean( $mysync, $exit_value ) ; + if ( $mysync->{ nb_errors } ) + { + myprint( errors_listing( $mysync ) ) ; + my $exit_value = exit_value( $mysync, $mysync->{ most_common_error } ) ; + exit_clean( $mysync, $exit_value ) ; + } + else + { + exit_clean( $mysync, $EX_OK ) ; + } return ; } -sub exit_clean +sub exit_clean { my $mysync = shift @ARG ; my $status = shift @ARG ; @@ -7006,26 +7871,27 @@ sub exit_clean myprint( "Exiting with return value $status ($EXIT_TXT{$status}) $mysync->{nb_errors}/$mysync->{errorsmax} nb_errors/max_errors PID $PROCESS_ID\n" ) ; cleanup_before_exit( $mysync ) ; - exit $status ; + #sleep( 3 ) ; + exit( $status ) ; } -sub missing_option +sub missing_option { - my $mysync = shift ; - my $option = shift ; - $mysync->{nb_errors}++ ; + my $mysync = shift @ARG ; + my $option = shift @ARG ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EX_USAGE, "$option option is mandatory, for help run $PROGRAM_NAME --help\n" ) ; return ; -} +} sub catch_ignore { - my $mysync = shift ; - my $signame = shift ; + my $mysync = shift @ARG ; + my $signame = shift @ARG ; my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", mygetppid( ), "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; do_and_print_stats( $mysync ) ; return ; @@ -7033,17 +7899,17 @@ sub catch_ignore -sub catch_exit +sub catch_exit { - my $mysync = shift ; + my $mysync = shift @ARG ; my $signame = shift || q{} ; if ( $signame ) { - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", mygetppid( ), "). Asked to terminate\n" ) ; - if ( $mysync->{can_do_stats} ) { + if ( $mysync->{ can_do_stats } ) { do_and_print_stats( $mysync ) ; myprint( "Ended by a signal $signame (my PID is $PROCESS_ID my PPID is ", - getppid( ), "). I am asked to terminate immediately.\n" ) ; + mygetppid( ), "). I am asked to terminate immediately.\n" ) ; } myprint( "You should resynchronize those accounts by running a sync again,\n", "since some messages and entire folders might still be missing on host2.\n" @@ -7060,34 +7926,34 @@ sub catch_exit #kill( 'TERM', $PROCESS_ID ) ; #sleep 1 ; #while ( 1 ) { } ; - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_BY_SIGNAL, "Still there after killing myself with signal $signame...\n" ) ; } else { - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_BY_SIGNAL, "Exiting in catch_exit with no signal...\n" ) ; } return ; -} +} sub catch_print { - my $mysync = shift ; - my $signame = shift ; + my $mysync = shift @ARG ; + my $signame = shift @ARG ; my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", mygetppid( ), "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; return ; } sub here_twice { - my $mysync = shift ; + my $mysync = shift @ARG ; my $now = time ; my $previous = $mysync->{lastcatch} || 0 ; $mysync->{lastcatch} = $now ; @@ -7100,55 +7966,58 @@ sub here_twice } -sub catch_reconnect +sub catch_reconnect { - my $mysync = shift ; - my $signame = shift ; - if ( here_twice( $mysync ) ) { + my $mysync = shift @ARG ; + my $signame = shift @ARG ; + if ( here_twice( $mysync ) ) + { myprint( "Got two signals $signame within $INTERVAL_TO_EXIT seconds. Exiting...\n" ) ; catch_exit( $mysync, $signame ) ; - }else{ - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), ")\n", + } + else + { + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", mygetppid( ), ")\n", "Hit 2 ctr-c within 2 seconds to exit the program\n", "Hit only 1 ctr-c to reconnect to both imap servers\n", ) ; myprint( "For now only one signal $signame within $INTERVAL_TO_EXIT seconds.\n" ) ; - if ( ! defined $mysync->{imap1} ) { return ; } - if ( ! defined $mysync->{imap2} ) { return ; } + if ( ! defined $mysync->{ imap1 } ) { return ; } + if ( ! defined $mysync->{ imap2 } ) { return ; } myprint( "Info: reconnecting to host1 imap server $mysync->{host1}\n" ) ; - $mysync->{imap1}->State( Mail::IMAPClient::Unconnected ) ; - $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - if ( $mysync->{imap1}->reconnect( ) ) + $mysync->{ imap1 }->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{ imap1 }->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{ imap1 }->reconnect( ) ) { myprint( "Info: reconnected to host1 imap server $mysync->{host1}\n" ) ; } else { - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; } myprint( "Info: reconnecting to host2 imap server\n" ) ; - $mysync->{imap2}->State( Mail::IMAPClient::Unconnected ) ; - $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - if ( $mysync->{imap2}->reconnect( ) ) + $mysync->{ imap2 }->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{ imap2 }->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{ imap2 }->reconnect( ) ) { myprint( "Info: reconnected to host2 imap server $mysync->{host2}\n" ) ; } else { - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; } myprint( "Info: reconnected to both imap servers\n" ) ; } return ; -} +} sub install_signals { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( under_docker_context( $mysync ) ) { @@ -7187,13 +8056,13 @@ sub tests_reconnect_12_if_needed my $mysync ; - $mysync->{imap1} = Mail::IMAPClient->new( ) ; - $mysync->{imap2} = Mail::IMAPClient->new( ) ; - $mysync->{imap1}->Server( 'test1.lamiral.info' ) ; - $mysync->{imap2}->Server( 'test2.lamiral.info' ) ; + $mysync->{ imap1 } = Mail::IMAPClient->new( ) ; + $mysync->{ imap2 } = Mail::IMAPClient->new( ) ; + $mysync->{ imap1 }->Server( 'test1.lamiral.info' ) ; + $mysync->{ imap2 }->Server( 'test2.lamiral.info' ) ; is( 2, reconnect_12_if_needed( $mysync ), 'reconnect_12_if_needed: test1&test2 .lamiral.info => 1' ) ; - is( 1, $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test1.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; - is( 1, $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test2.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; + is( 1, $mysync->{ imap1 }->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test1.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; + is( 1, $mysync->{ imap2 }->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test2.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; note( 'Leaving tests_reconnect_12_if_needed()' ) ; return ; @@ -7201,12 +8070,14 @@ sub tests_reconnect_12_if_needed sub reconnect_12_if_needed { - my $mysync = shift ; + my $mysync = shift @ARG ; #return 2 ; - if ( ! reconnect_if_needed( $mysync->{imap1} ) ) { + if ( ! reconnect_if_needed( $mysync->{ imap1 } ) ) + { return ; } - if ( ! reconnect_if_needed( $mysync->{imap2} ) ) { + if ( ! reconnect_if_needed( $mysync->{ imap2 } ) ) + { return ; } # both were good @@ -7240,14 +8111,16 @@ sub reconnect_if_needed # return undef upon failure. # return 1 upon connection success, with or without reconnection. - my $imap = shift ; + my $imap = shift @ARG ; if ( ! defined $imap ) { return ; } if ( ! $imap->Server( ) ) { return ; } - if ( $imap->IsUnconnected( ) ) { + if ( $imap->IsUnconnected( ) ) + { $imap->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - if ( $imap->reconnect( ) ) { + if ( $imap->reconnect( ) ) + { return 1 ; } }else{ @@ -7258,7 +8131,8 @@ sub reconnect_if_needed $imap->State( Mail::IMAPClient::Unconnected ) ; $imap->reconnect( ) ; $imap->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - if ( $imap->noop ) { + if ( $imap->noop ) + { # NOOP is ok return 1 ; } @@ -7269,7 +8143,7 @@ sub reconnect_if_needed sub justconnect { - my $mysync = shift ; + my $mysync = shift @ARG ; my $justconnect1 = justconnect1( $sync ) ; my $justconnect2 = justconnect2( $sync ) ; return "$justconnect1 $justconnect2"; @@ -7277,18 +8151,20 @@ sub justconnect sub justconnect1 { - my $mysync = shift ; - if ( $mysync->{host1} ) + my $mysync = shift @ARG ; + if ( $mysync->{ host1 } ) { - myprint( "Host1: Will just connect to $mysync->{host1} without login\n" ) ; - $mysync->{imap1} = connect_imap( - $mysync->{host1}, $mysync->{port1}, - $mysync->{ssl1}, $mysync->{tls1}, - $mysync->{ acc1 } ) ; + myprint( "Host1: Will just connect to $mysync->{ host1 } without login\n" ) ; + $mysync->{ imap1 } = connect_imap( + $mysync->{ host1 }, $mysync->{ port1 }, + $mysync->{ ssl1 }, $mysync->{ tls1 }, + $mysync->{ acc1 } + ) ; - imap_id( $mysync, $mysync->{imap1}, $mysync->{ acc1 }->{ Side } ) ; - $mysync->{imap1}->logout( ) ; - return $mysync->{host1} ; + imap_id( $mysync, $mysync->{ imap1 }, $mysync->{ acc1 }->{ Side } ) ; + $mysync->{ imap1 }->logout( ) ; + $mysync->{ acc1 }->{ justconnectdone } = 1 ; + return $mysync->{ host1 } ; } return q{} ; @@ -7296,28 +8172,126 @@ sub justconnect1 sub justconnect2 { - my $mysync = shift ; - if ( $mysync->{host2} ) + my $mysync = shift @ARG ; + if ( $mysync->{ host2 } ) { - myprint( "Host2: Will just connect to $mysync->{host2} without login\n" ) ; - $mysync->{imap2} = connect_imap( - $mysync->{host2}, $mysync->{port2}, - $mysync->{ssl2}, $mysync->{tls2}, - $mysync->{ acc2 } ) ; + myprint( "Host2: Will just connect to $mysync->{ host2 } without login\n" ) ; + $mysync->{ imap2 } = connect_imap( + $mysync->{ host2 }, $mysync->{ port2 }, + $mysync->{ ssl2 }, $mysync->{ tls2 }, + $mysync->{ acc2 } + ) ; - imap_id( $mysync, $mysync->{imap2}, $mysync->{ acc2 }->{ Side } ) ; - $mysync->{imap2}->logout( ) ; - return $mysync->{host2} ; + imap_id( $mysync, $mysync->{ imap2 }, $mysync->{ acc2 }->{ Side } ) ; + $mysync->{ imap2 }->logout( ) ; + $mysync->{ acc2 }->{ justconnectdone } = 1 ; + return $mysync->{ host2 } ; } return q{} ; } + +sub tests_trigger_justconnect1 +{ + note( 'Entering tests_trigger_justconnect1()' ) ; + + is( undef, trigger_justconnect1( ), 'tests_trigger_justconnect1: no args => undef' ) ; + my $mysync = { } ; + + is( undef, trigger_justconnect1( $mysync ), 'tests_trigger_justconnect1: { } => undef' ) ; + + $mysync->{ justconnect } = 1 ; + is( 1, trigger_justconnect1( $mysync ), 'tests_trigger_justconnect1: --justconnect => 1' ) ; + + delete $mysync->{ justconnect } ; + is( undef, trigger_justconnect1( $mysync ), 'tests_trigger_justconnect1: no more --justconnect => undef' ) ; + + $mysync->{ host1 } = 'hhh' ; + is( 1, trigger_justconnect1( $mysync ), 'tests_trigger_justconnect1: just --host1 => 1' ) ; + + $mysync->{ user1 } = 'uuu' ; + is( undef, trigger_justconnect1( $mysync ), 'tests_trigger_justconnect1: --host1 --user1 => 1' ) ; + + note( 'Leaving tests_trigger_justconnect1()' ) ; + return ; +} + + +sub trigger_justconnect1 +{ + my $mysync = shift @ARG ; + + if ( ! $mysync ) { return ; } + + if ( ! scalar( %{ $mysync } ) ) { return ; } + + if ( $mysync->{ justconnect } ) { return 1 ; } + + if ( + not $mysync->{ user1 } and not $mysync->{ acc1 }->{ oauthdirect } + ) + { + return 1 ; + } + + return ; +} + + +sub tests_trigger_justconnect2 +{ + note( 'Entering tests_trigger_justconnect2()' ) ; + + is( undef, trigger_justconnect2( ), 'tests_trigger_justconnect2: no args => undef' ) ; + my $mysync = { } ; + + is( undef, trigger_justconnect2( $mysync ), 'tests_trigger_justconnect2: { } => undef' ) ; + + $mysync->{ justconnect } = 1 ; + is( 1, trigger_justconnect2( $mysync ), 'tests_trigger_justconnect2: --justconnect => 1' ) ; + + delete $mysync->{ justconnect } ; + is( undef, trigger_justconnect2( $mysync ), 'tests_trigger_justconnect2: no more --justconnect => undef' ) ; + + $mysync->{ host2 } = 'aaa' ; + is( 1, trigger_justconnect2( $mysync ), 'tests_trigger_justconnect2: just --host2 => 1' ) ; + + $mysync->{ user2 } = 'uuu' ; + is( undef, trigger_justconnect2( $mysync ), 'tests_trigger_justconnect1: --host2 --user2 => 1' ) ; + + note( 'Leaving tests_trigger_justconnect2()' ) ; + return ; +} + + +sub trigger_justconnect2 +{ + my $mysync = shift @ARG ; + + if ( ! $mysync ) { return ; } + + if ( ! scalar( %{ $mysync } ) ) { return ; } + + if ( $mysync->{ justconnect } ) { return 1 ; } + + if ( + not $mysync->{ user2 } and not $mysync->{ acc2 }->{ oauthdirect } + ) + { + return 1 ; + } + + return ; +} + + + sub skip_macosx { #return ; # hostname is sometimes "macosx.polarhome.com" sometimes "macosx" - return( ( ( 'macosx.polarhome.com' eq hostname( ) ) || ( 'macosx' eq hostname( ) ) ) + return( ( ( 'macosx.polarhome.com' eq hostname( ) ) || ( 'macosx' eq hostname( ) ) ) && ( 'darwin' eq $OSNAME ) ) ; } @@ -7327,132 +8301,137 @@ sub skip_macosx_binary return( skip_macosx( ) && ( $PROGRAM_NAME =~ m{imapsync_bin_Darwin} ) ) ; } - - -sub tests_mailimapclient_connect +sub skip_ks5 { + return( 'ks5' eq hostname( ) ); +} + +sub skip_docker +{ + return( -e '/Dockerfile' ) ; +} + + +sub tests_mailimapclient_connect +{ note( 'Entering tests_mailimapclient_connect()' ) ; my $imap ; # ipv4 - ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv4: new' ) ; - is( 'Mail::IMAPClient', ref( $imap ), 'mailimapclient_connect ipv4: ref is Mail::IMAPClient' ) ; + ok( $imap = Mail::IMAPClient->new( ), 'tests_mailimapclient_connect: ipv4 new' ) ; + is( 'Mail::IMAPClient', ref( $imap ), 'tests_mailimapclient_connect: ipv4 ref is Mail::IMAPClient' ) ; # Mail::IMAPClient 3.40 die on this... So we skip it, thanks to "mature" IO::Socket::IP # Mail::IMAPClient 3.42 is ok so this test is back. - is( undef, $imap->connect( ), 'mailimapclient_connect ipv4: connect with no server => failure' ) ; + is( undef, $imap->connect( ), 'tests_mailimapclient_connect: ipv4 connect with no server => failure' ) ; - is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'mailimapclient_connect ipv4: setting Server(test.lamiral.info)' ) ; - is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4: setting Debug( 1 )' ) ; - is( 143, $imap->Port( 143 ), 'mailimapclient_connect ipv4: setting Port( 143 )' ) ; - is( 10, $imap->Timeout( 10 ), 'mailimapclient_connect ipv4: setting Timeout( 10 )' ) ; - like( ref( $imap->connect( ) ), qr/IO::Socket::INET|IO::Socket::IP/, 'mailimapclient_connect ipv4: connect to test.lamiral.info' ) ; - like( $imap->logout( ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv4: logout' ) ; - is( undef, undef $imap, 'mailimapclient_connect ipv4: free variable' ) ; + is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'tests_mailimapclient_connect: ipv4 setting Server(test.lamiral.info)' ) ; + is( 1, $imap->Debug( 1 ), 'tests_mailimapclient_connect: ipv4 setting Debug( 1 )' ) ; + is( 143, $imap->Port( 143 ), 'tests_mailimapclient_connect: ipv4 setting Port( 143 )' ) ; + is( 10, $imap->Timeout( 10 ), 'tests_mailimapclient_connect: ipv4 setting Timeout( 10 )' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::INET|IO::Socket::IP/, 'tests_mailimapclient_connect: ipv4 connect to test.lamiral.info' ) ; + like( $imap->logout( ), qr/Mail::IMAPClient/, 'tests_mailimapclient_connect: ipv4 logout' ) ; + is( undef, undef $imap, 'tests_mailimapclient_connect: ipv4 free variable' ) ; # ipv4 + ssl - ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv4 + ssl: new' ) ; - is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'mailimapclient_connect ipv4 + ssl: setting Server(test.lamiral.info)' ) ; - is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4 + ssl: setting Debug( 1 )' ) ; - ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'mailimapclient_connect ipv4 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; - is( 993, $imap->Port( 993 ), 'mailimapclient_connect ipv4 + ssl: setting Port( 993 )' ) ; - like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv4 + ssl: connect to test.lamiral.info' ) ; - like( $imap->logout( ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv4 + ssl: logout in ssl does not cause failure' ) ; - is( undef, undef $imap, 'mailimapclient_connect ipv4 + ssl: free variable' ) ; + ok( $imap = Mail::IMAPClient->new( ), 'tests_mailimapclient_connect: ipv4 + ssl: new' ) ; + is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'tests_mailimapclient_connect: ipv4 + ssl: setting Server(test.lamiral.info)' ) ; + is( 1, $imap->Debug( 1 ), 'tests_mailimapclient_connect: ipv4 + ssl: setting Debug( 1 )' ) ; + ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'tests_mailimapclient_connect: ipv4 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; + is( 993, $imap->Port( 993 ), 'tests_mailimapclient_connect: ipv4 + ssl: setting Port( 993 )' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'tests_mailimapclient_connect: ipv4 + ssl: connect to test.lamiral.info' ) ; + like( $imap->logout( ), qr/Mail::IMAPClient/, 'tests_mailimapclient_connect: ipv4 + ssl: logout in ssl does not cause failure' ) ; + is( undef, undef $imap, 'tests_mailimapclient_connect: ipv4 + ssl: free variable' ) ; # ipv6 + ssl - ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv6 + ssl: new' ) ; - is( 'petiteipv6.lamiral.info', $imap->Server( 'petiteipv6.lamiral.info' ), 'mailimapclient_connect ipv6 + ssl: setting Server petiteipv6.lamiral.info' ) ; - is( 10, $imap->Timeout( 10 ), 'mailimapclient_connect ipv6: setting Timeout( 10 )' ) ; - ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'mailimapclient_connect ipv6 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; - is( 993, $imap->Port( 993 ), 'mailimapclient_connect ipv6 + ssl: setting Port( 993 )' ) ; + ok( $imap = Mail::IMAPClient->new( ), 'tests_mailimapclient_connect: ipv6 + ssl: new' ) ; + is( 'test1ipv6.lamiral.info', $imap->Server( 'test1ipv6.lamiral.info' ), 'tests_mailimapclient_connect: ipv6 + ssl: setting Server test1ipv6.lamiral.info' ) ; + is( 10, $imap->Timeout( 10 ), 'tests_mailimapclient_connect: ipv6 setting Timeout( 10 )' ) ; + ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'tests_mailimapclient_connect: ipv6 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; + is( 993, $imap->Port( 993 ), 'tests_mailimapclient_connect: ipv6 + ssl: setting Port( 993 )' ) ; SKIP: { - if ( - 'CUILLERE' eq hostname() - or - skip_macosx() - or - -e '/.dockerenv' - or - 'pcHPDV7-HP' eq hostname() + if ( + 'CUILLERE' eq hostname( ) + or + skip_macosx( ) + or + skip_docker( ) + or + 'pcHPDV7-HP' eq hostname( ) + or '5521.local' eq hostname( ) ) - { - skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 4 ) ; + { + skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/polarhome/macstadium/docker cannot do ipv6', 4 ) ; + } + + is( 1, $imap->Debug( 1 ), 'tests_mailimapclient_connect: ipv4 + ssl: setting Debug( 1 )' ) ; + + # It sounds stupid but sometimes it avoids failures on the next test about $imap->connect + is( '2001:41d0:2:84e0::1', resolv( 'test1ipv6.lamiral.info' ), 'resolv: test1ipv6.lamiral.info => 2a01:e0a:c59:f090:f5eb:1776:332:41d5' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'tests_mailimapclient_connect: ipv6 + ssl: connect to test1ipv6.lamiral.info' ) ; + # This one is ok on petite, not on ks2, do not know why, so commented. + like( ref( $imap->logout( ) ), qr/Mail::IMAPClient/, 'tests_mailimapclient_connect: ipv6 + ssl: logout in ssl is ok on test1ipv6.lamiral.info' ) ; } - is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4 + ssl: setting Debug( 1 )' ) ; - - # It sounds stupid but it avoids failures on the next test about $imap->connect - is( '2a01:e34:ecde:70d0:223:54ff:fec2:36d7', resolv( 'petiteipv6.lamiral.info' ), 'resolv: petiteipv6.lamiral.info => 2a01:e34:ecde:70d0:223:54ff:fec2:36d7' ) ; - - like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv6 + ssl: connect to petiteipv6.lamiral.info' ) ; - # This one is ok on petite, not on ks2, do not know why, so commented. - like( ref( $imap->logout( ) ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv6 + ssl: logout in ssl is ok on petiteipv6.lamiral.info' ) ; - } - - is( undef, undef $imap, 'mailimapclient_connect ipv6 + ssl: free variable' ) ; - - + is( undef, undef $imap, 'tests_mailimapclient_connect: ipv6 + ssl: free variable' ) ; note( 'Leaving tests_mailimapclient_connect()' ) ; return ; -} +} -sub tests_mailimapclient_connect_bug -{ +sub tests_mailimapclient_connect_bug +{ note( 'Entering tests_mailimapclient_connect_bug()' ) ; my $imap ; # ipv6 - ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect_bug ipv6: new' ) ; - is( 'ks6ipv6.lamiral.info', $imap->Server( 'ks6ipv6.lamiral.info' ), 'mailimapclient_connect_bug ipv6: setting Server(ks6ipv6.lamiral.info)' ) ; - is( 143, $imap->Port( 143 ), 'mailimapclient_connect_bug ipv6: setting Port( 993 )' ) ; + ok( $imap = Mail::IMAPClient->new( ), 'tests_mailimapclient_connect_bug: ipv6: new' ) ; + is( 'ks6ipv6.lamiral.info', $imap->Server( 'ks6ipv6.lamiral.info' ), 'tests_mailimapclient_connect_bug: ipv6: setting Server(ks6ipv6.lamiral.info)' ) ; + is( 143, $imap->Port( 143 ), 'tests_mailimapclient_connect_bug: ipv6: setting Port( 993 )' ) ; SKIP: { if ( - 'CUILLERE' eq hostname() + 'CUILLERE' eq hostname( ) or - skip_macosx() + skip_macosx( ) or - -e '/.dockerenv' - or - 'pcHPDV7-HP' eq hostname() + 'pcHPDV7-HP' eq hostname( ) ) { skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 1 ) ; } - like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'mailimapclient_connect_bug ipv6: connect to ks6ipv6.lamiral.info' ) - or diag( 'mailimapclient_connect_bug ipv6: ', $imap->LastError( ), $!, ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'tests_mailimapclient_connect_bug: ipv6: connect to ks6ipv6.lamiral.info' ) + or diag( 'tests_mailimapclient_connect_bug: ipv6: ', $imap->LastError( ), $!, ) ; } - #is( $imap->logout( ), undef, 'mailimapclient_connect_bug ipv6: logout in ssl causes failure' ) ; - is( undef, undef $imap, 'mailimapclient_connect_bug ipv6: free variable' ) ; + #is( $imap->logout( ), undef, 'tests_mailimapclient_connect_bug: ipv6: logout in ssl causes failure' ) ; + is( undef, undef $imap, 'tests_mailimapclient_connect_bug: ipv6: free variable' ) ; note( 'Leaving tests_mailimapclient_connect_bug()' ) ; return ; -} +} -sub tests_connect_socket -{ +sub tests_connect_socket +{ note( 'Entering tests_connect_socket()' ) ; - is( undef, connect_socket( ), 'connect_socket: no args' ) ; + is( undef, connect_socket( ), 'tests_connect_socket: no args' ) ; my $socket ; my $imap ; SKIP: { if ( - 'CUILLERE' eq hostname() + 'CUILLERE' eq hostname( ) or - skip_macosx() + skip_macosx( ) or - -e '/.dockerenv' + skip_docker( ) or - 'pcHPDV7-HP' eq hostname() + 'pcHPDV7-HP' eq hostname( ) ) { skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 2 ) ; @@ -7464,14 +8443,14 @@ sub tests_connect_socket ) ; - ok( $imap = connect_socket( $socket ), 'connect_socket: ks6ipv6.lamiral.info port 143 IO::Socket::INET6' ) ; + ok( $imap = connect_socket( $socket ), 'tests_connect_socket: ks6ipv6.lamiral.info port 143 IO::Socket::INET6' ) ; #$imap->Debug( 1 ) ; # myprint( $imap->capability( ) ) ; if ( $imap ) { $imap->logout( ) ; } - $IO::Socket::SSL::DEBUG = 4 ; + #$IO::Socket::SSL::DEBUG = 4 ; $socket = IO::Socket::SSL->new( PeerHost => 'ks6ipv6.lamiral.info', PeerPort => 993, @@ -7479,7 +8458,7 @@ sub tests_connect_socket SSL_cipher_list => 'DEFAULT:!DH', ) ; # myprint( $socket ) ; - ok( $imap = connect_socket( $socket ), 'connect_socket: ks6ipv6.lamiral.info port 993 IO::Socket::SSL' ) ; + ok( $imap = connect_socket( $socket ), 'tests_connect_socket: ks6ipv6.lamiral.info port 993 IO::Socket::SSL' ) ; #$imap->Debug( 1 ) ; # myprint( $imap->capability( ) ) ; # $socket->close( ) ; @@ -7493,7 +8472,7 @@ sub tests_connect_socket } note( 'Leaving tests_connect_socket()' ) ; return ; -} +} sub connect_socket { @@ -7513,70 +8492,64 @@ sub connect_socket } + + sub tests_probe_imapssl -{ +{ note( 'Entering tests_probe_imapssl()' ) ; - is( undef, probe_imapssl( ), 'probe_imapssl: no args => undef' ) ; - is( undef, probe_imapssl( 'unknown' ), 'probe_imapssl: unknown => undef' ) ; - - note( "hostname is: ", hostname() ) ; - SKIP: { - if ( - 'CUILLERE' eq hostname() - or - skip_macosx() - or - -e '/.dockerenv' - or - 'pcHPDV7-HP' eq hostname() - ) - { - skip( 'Tests avoided on CUILLERE or pcHPDV7-HP or Mac or docker: cannot do ipv6', 0 ) ; - } - # fed up with this one - #like( probe_imapssl( 'ks6ipv6.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: ks6ipv6.lamiral.info matches "* OK"' ) ; - } ; + is( undef, probe_imapssl( ), 'tests_probe_imapssl: no args => undef' ) ; + is( undef, probe_imapssl( 'unknown' ), 'tests_probe_imapssl: unknown => undef' ) ; + note( "hostname is: ", hostname( ) ) ; # It sounds stupid but it avoids failures on the next test about $imap->connect - ok( resolv( 'imap.gmail.com' ), 'resolv: imap.gmail.com => something' ) ; - like( probe_imapssl( 'imap.gmail.com' ), qr/^\* OK/, 'probe_imapssl: imap.gmail.com matches "* OK"' ) ; - - like( probe_imapssl( 'test1.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: test1.lamiral.info matches "* OK"' ) ; + ok( resolv( 'imap.gmail.com' ), 'tests_probe_imapssl: resolv imap.gmail.com => something' ) ; + like( probe_imapssl( 'imap.gmail.com' ), qr/^\* OK/, 'tests_probe_imapssl: imap.gmail.com => matches "* OK"' ) ; + like( probe_imapssl( 'test1.lamiral.info' ), qr/^\* OK/, 'tests_probe_imapssl: test1.lamiral.info => matches "* OK"' ) ; + like( probe_imapssl( 'imap.gmail.com', 993 ), qr/^\* OK/, 'tests_probe_imapssl: imap.gmail.com port 993 => matches "* OK"' ) ; + like( probe_imapssl( 'test1.lamiral.info', 993 ), qr/^\* OK/, 'tests_probe_imapssl: test1.lamiral.info port 993 => matches "* OK"' ) ; + is( undef, probe_imapssl( 'test1.lamiral.info', 143 ), 'tests_probe_imapssl: test1.lamiral.info port 143 => failure' ) ; note( 'Leaving tests_probe_imapssl()' ) ; return ; } -sub probe_imapssl + +sub probe_imapssl { - my $host = shift ; + my $host = shift @ARG ; + my $port = shift @ARG || $IMAP_SSL_PORT ; if ( ! $host ) { return ; } - $sync->{ debug } and $IO::Socket::SSL::DEBUG = 4 ; + #$sync->{ debug } and $IO::Socket::SSL::DEBUG = 4 ; my $socket = IO::Socket::SSL->new( PeerHost => $host, - PeerPort => $IMAP_SSL_PORT, + PeerPort => $port, SSL_verifycn_scheme => 'imap', SSL_verify_mode => $SSL_VERIFY_POLICY, SSL_cipher_list => 'DEFAULT:!DH', ) ; - if ( ! $socket ) { return ; } - $sync->{ debug } and print "socket: $socket\n" ; + if ( ! $socket ) + { + myprint( "Probing imap ssl on host $host port $port, got failure.\n" ) ; + return ; + } + $sync->{ debug } and myprint( "socket: $socket\n" ) ; my $banner ; $socket->sysread( $banner, 65_536 ) ; - $sync->{ debug } and print "banner: $banner" ; + myprint( "Probing imap ssl on host $host port $port, got banner: $banner" ) ; $socket->close( ) ; return $banner ; -} +} -sub connect_imap + +sub connect_imap { - my( $host, $port, $ssl, $tls, $acc ) = @_ ; + my( $host, $port, $ssl, $tls, $acc ) = @ARG ; my $imap = Mail::IMAPClient->new( ) ; if ( $ssl ) { set_ssl( $imap, $acc ) } @@ -7584,9 +8557,9 @@ sub connect_imap $imap->Port( $port ) ; $imap->Debug( $acc->{ debugimap } ) ; $imap->Timeout( $acc->{ timeout } ) ; - + #$imap->Keepalive( $acc->{ keepalive } ) ; - + my $side = lc $acc->{ Side } ; @@ -7594,25 +8567,30 @@ sub connect_imap if ( ! $imap->connect( ) ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_CONNECTION_FAILURE, - "$acc->{ Side }: Can not open imap connection on [$host]: ", + "$acc->{ Side }: Can not open imap connection on [$host] port [$port] : ", $imap->LastError, " $OS_ERROR\n" ) ; } - myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), "\n" ) ; - + myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), " Local IP address: ", $imap->Socket->sockhost(), "\n" ) ; my $banner = $imap->Results()->[0] ; myprint( "$acc->{ Side } banner: $banner" ) ; myprint( "$acc->{ Side } capability: ", join(q{ }, @{ $imap->capability() || [] }), "\n" ) ; + if ( ( ! $ssl ) and ( ! defined $tls ) and $imap->has_capability( 'STARTTLS' ) ) + { + myprint( "$acc->{ Side }: going to ssl because STARTTLS is in CAPABILITY. Use --notls1 or --notls2 to avoid that behavior\n" ) ; + $tls = 1 ; + } + if ( $tls ) { set_tls( $imap, $acc ) ; if ( ! $imap->starttls( ) ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_TLS_FAILURE, "$acc->{ Side }: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" @@ -7621,62 +8599,110 @@ sub connect_imap myprint( "$acc->{ Side }: Socket successfully converted to SSL\n" ) ; } return( $imap ) ; -} +} + + + + + + + +sub tests_compress_starttls +{ + note( 'Entering tests_compress_starttls()' ) ; + + SKIP: + { + if ( skip_macosx( ) ) + { + skip( 'Tests avoided on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 8 ) ; + } + else + { + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + $acc->{ debugimap } = 1 ; + $acc->{ compress } = 1 ; + $acc->{ N } = 'K' ; + $acc->{ sslargs } = { + SSL_verify_mode => 0, + } ; + + #$IO::Socket::SSL::DEBUG = 4 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 1, + 1, 100, $acc, {}, + ), 'tests_compress_starttls: test1.lamiral.info test1 tls' + ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'tests_compress_starttls: test1.lamiral.info test1 tls IsAuthenticated' ) ; + + is( $acc->{ imap }, acc_compress_imap( $acc ), "tests_compress_starttls: test1.lamiral.info tls compression ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress_starttls: test1.lamiral.info tls compression 2nd time call nok" ) ; + + # Third, no compression + $acc->{ compress } = 0 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 1, + 1, 100, $acc, {}, + ), 'tests_compress_starttls: test1.lamiral.info test1 tls no compression' + ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_compress_starttls: test1.lamiral.info test1 tls no compression IsAuthenticated' ) ; + + is( undef, acc_compress_imap( $acc ), "tests_compress_starttls: test1.lamiral.info no compression ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress_starttls: test1.lamiral.info 2nd no compression ok" ) ; + } + } + note( 'Leaving tests_compress_starttls()' ) ; + return ; +} + sub tests_compress_ssl { note( 'Entering tests_compress_ssl()' ) ; SKIP: { - if ( skip_macosx( ) ) - { - skip( 'Tests avoided on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 12 ) ; - } - else - { - my $myimap ; - my $acc = {} ; - $acc->{ Side } = 'HostK' ; - $acc->{ authmech } = 'LOGIN' ; - $acc->{ debugimap } = 1 ; - $acc->{ compress } = 1 ; - $acc->{ N } = 'K' ; + if ( skip_macosx( ) ) + { + skip( 'Tests avoided on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 8 ) ; + } + else + { + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + $acc->{ debugimap } = 1 ; + $acc->{ compress } = 1 ; + $acc->{ N } = 'K' ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'tests_compress_ssl: test1.lamiral.info test1 ssl' + ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_compress_ssl: test1.lamiral.info test1 ssl IsAuthenticated' ) ; - ok( - $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', - 1, undef, - 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; - - - is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd nok" ) ; - - ok( - $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', - 0, undef, - 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1 tls' ) ; - ok( $myimap && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; - - is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls 2nd nok" ) ; - - # Third, no compression - $acc->{ compress } = 0 ; - ok( - $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', - 1, undef, - 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; - - - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info off ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd off ok" ) ; - - } + is( $acc->{ imap }, acc_compress_imap( $acc ), "tests_compress_ssl: test1.lamiral.info compression ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress_ssl: test1.lamiral.info compression 2nd time call nok" ) ; + + # Third, no compression + $acc->{ compress } = 0 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'tests_compress_ssl: test1.lamiral.info test1 ssl' + ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_compress_ssl: test1.lamiral.info test1 ssl no compression IsAuthenticated' ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress_ssl: test1.lamiral.info no compression ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress_ssl: test1.lamiral.info 2nd no compression ok" ) ; + } } note( 'Leaving tests_compress_ssl()' ) ; return ; @@ -7688,46 +8714,47 @@ sub tests_compress my $myimap ; my $acc = {} ; - $acc->{ Side } = 'HostK' ; + $acc->{ Side } = 'HostK' ; $acc->{ authmech } = 'LOGIN' ; $acc->{ debugimap } = 1 ; $acc->{ compress } = 1 ; $acc->{ N } = 'K' ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, {}, + ), 'tests_compress: test1.lamiral.info test1' + ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_compress: test1.lamiral.info test1 IsAuthenticated' ) ; + + + is( $acc->{ imap }, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info 2nd nok" ) ; ok( $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', 0, 0, 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 IsAuthenticated' ) ; - - - is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd nok" ) ; - - ok( - $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', - 0, 0, - 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1 tls' ) ; - ok( $myimap && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; - - is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls 2nd nok" ) ; + ), 'tests_compress: test1.lamiral.info test1 tls' ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'tests_compress: test1.lamiral.info test1 tls IsAuthenticated' ) ; + is( $acc->{ imap }, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info tls ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info tls 2nd nok" ) ; + # Third, no compression $acc->{ compress } = 0 ; ok( $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', 0, 0, 1, 100, $acc, {}, - ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + ), 'tests_compress: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_compress: test1.lamiral.info test1 ssl IsAuthenticated' ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info off ok" ) ; - is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd off ok" ) ; - + is( undef, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info off ok" ) ; + is( undef, acc_compress_imap( $acc ), "tests_compress: test1.lamiral.info 2nd off ok" ) ; + note( 'Leaving tests_compress()' ) ; return ; } @@ -7735,14 +8762,14 @@ sub tests_compress sub acc_compress_imap { - my $acc = shift ; - + my $acc = shift @ARG ; + if ( ! defined( $acc ) ) { return ; } - + my $ret ; my $imap = $acc->{ imap } ; if ( ! defined $imap ) { return ; } - + if ( $imap && $acc->{ compress } ) { myprint( "$acc->{ Side }: Trying to turn imap compression on. Use --nocompress" . $acc->{ N } . " to avoid compression on " . lc( $acc->{ Side } ) . "\n" ) ; @@ -7763,119 +8790,225 @@ sub acc_compress_imap return $ret ; } -sub tests_login_imap + +# Each month (trimester?): +# echo | openssl s_client -crlf -connect test1.lamiral.info:993 +# ... +# certificate has expired +# Fix: ssh root@test1.lamiral.info 'apt update && apt upgrade && /etc/init.d/dovecot restart' +# +# or this one: +# echo | openssl s_client -crlf -connect test1.lamiral.info:993 +# ... +# Verify return code: 9 (certificate is not yet valid) +# Fix: /etc/init.d/openntpd restart +# 2021_09_04 done + + +sub tests_login_imap_notls_nossl { - note( 'Entering tests_login_imap()' ) ; + note( 'Entering tests_login_imap_notls_nossl()' ) ; - is( undef, login_imap( ), 'login_imap: no args => undef' ) ; + is( undef, login_imap( ), 'tests_login_imap_notls_nossl: no args => undef' ) ; - SKIP: { - if ( skip_macosx( ) ) + SKIP: { - skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 15 ) ; + if ( skip_macosx( ) ) + { + skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 10 ) ; + } + else + { + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + + + #login_imap( $host, $port, $user, $password, + # $ssl, $tls, + # $uid, $split, $acc, $mysync + + #$IO::Socket::SSL::DEBUG = 4 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, {}, + ), 'tests_login_imap_notls_nossl: test1.lamiral.info test1 tls' + ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'tests_login_imap_notls_nossl: test1.lamiral.info test1 tls IsAuthenticated' ) ; + is( $myimap, $acc->{ imap }, "tests_login_imap_notls_nossl: acc->{ imap } ok test1 tls") ; + + + # No name resolution + my $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host2' ; + $acc->{ authmech } = 'LOGIN' ; + is( + undef, login_imap( 'noresol.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_notls_nossl: noresol.lamiral.info undef' + ) ; + + is( 'ERR_CONNECTION_FAILURE_HOST2', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_notls_nossl: Host2 noresol.lamiral.info => ERR_CONNECTION_FAILURE_HOST2' ) ; + is( undef, $acc->{ imap }, "tests_login_imap_notls_nossl: acc->{ imap } noresol error => undef") ; + + # authentication failure for user2 + $mysync = {} ; + is( + undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', + 0, 0, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_notls_nossl: user2 bad passord => undef' + ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_notls_nossl: Host2 bad password => ERR_AUTHENTICATION_FAILURE_USER2' ) ; + + # authentication failure for user1 + $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ authmech } = 'LOGIN' ; + is( + undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', + 0, 0, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_notls_nossl: user1 bad passord => undef' + ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_notls_nossl: Host1 bad password => ERR_AUTHENTICATION_FAILURE_USER1' ) ; + } } - else{ - - my $myimap ; - my $acc = {} ; - $acc->{ Side } = 'HostK' ; - $acc->{ authmech } = 'LOGIN' ; - #$IO::Socket::SSL::DEBUG = 4 ; - # Each month (trimester?): - # echo | openssl s_client -crlf -connect test1.lamiral.info:993 - # ... - # certificate has expired - # Fix: ssh root@test1.lamiral.info 'apt update && apt upgrade && /etc/init.d/dovecot restart' - # - # or - # echo | openssl s_client -crlf -connect test1.lamiral.info:993 - # ... - # Verify return code: 9 (certificate is not yet valid) - # Fix: /etc/init.d/openntpd restart - # 2021_09_04 done - ok( - $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', - 1, undef, - 1, 100, $acc, {}, - ), 'login_imap: test1.lamiral.info test1 ssl' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; - - is( $myimap, $acc->{ imap }, "login_imap: acc->{ imap } ok test1 ssl") ; - - ok( - $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', - 0, undef, - 1, 100, $acc, {}, - ), 'login_imap: test1.lamiral.info test1 tls' ) ; - ok( $myimap && $myimap->IsAuthenticated( ), 'login_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; - is( $myimap, $acc->{ imap }, "login_imap: acc->{ imap } ok test1 tls") ; - - #$IO::Socket::SSL::DEBUG = 4 ; - $acc->{sslargs} = { SSL_version => 'SSLv2' } ; - # SSLv2 not supported - is( - undef, $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', - 0, undef, - 1, 100, $acc, {}, - ), 'login_imap: test1.lamiral.info test1 tls SSLv2 not supported' ) ; -#SSL_verify_mode => 1 -#SSL_version => 'TLSv1_1' - is( undef, $acc->{ imap }, "login_imap: acc->{ imap } test1 tls error => undef") ; - - - # I have left ? exit_clean to be replaced by errors_incr( $mysync, 'error message' ) - # 1 in login_imap() - - - my $mysync = {} ; - $acc = {} ; - $acc->{ Side } = 'Host2' ; - $acc->{ authmech } = 'LOGIN' ; - is( - undef, login_imap( 'noresol.lamiral.info', 143, 'test1', 'secret1', - 0, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: noresol.lamiral.info undef' ) ; - - is( 'ERR_CONNECTION_FAILURE_HOST2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 noresol.lamiral.info => ERR_CONNECTION_FAILURE_HOST2' ) ; - is( undef, $acc->{ imap }, "login_imap: acc->{ imap } noresol error => undef") ; - - # authentication failure for user2 - $mysync = {} ; - is( - undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', - 0, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: user2 bad passord => undef' ) ; - - is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 bad password => ERR_AUTHENTICATION_FAILURE_USER2' ) ; - - # authentication failure for user1 - $mysync = {} ; - $acc = {} ; - $acc->{ Side } = 'Host1' ; - $acc->{ authmech } = 'LOGIN' ; - is( - undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', - 0, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: user1 bad passord => undef' ) ; - - is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 bad password => ERR_AUTHENTICATION_FAILURE_USER1' ) ; - - } - } - - note( 'Leaving tests_login_imap()' ) ; + + note( 'Leaving tests_login_imap_notls_nossl()' ) ; return ; } + +sub tests_login_imap_starttls +{ + note( 'Entering tests_login_imap_starttls()' ) ; + + SKIP: + { + if ( skip_macosx( ) ) + { + skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 8 ) ; + } + else + { + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + + $acc->{ sslargs } = { + SSL_verify_mode => 0, + } ; + + #login_imap( $host, $port, $user, $password, + # $ssl, $tls, + # $uid, $split, $acc, $mysync + + #$IO::Socket::SSL::DEBUG = 4 ; + ok( + $myimap = login_imap( + 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 1, + 1, 100, $acc, {}, + ), 'tests_login_imap_starttls: test1.lamiral.info test1 tls' + ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'tests_login_imap_starttls: test1.lamiral.info test1 tls IsAuthenticated' ) ; + is( $myimap, $acc->{ imap }, "tests_login_imap_starttls: acc->{ imap } ok test1 tls") ; + + # STARTTLS port 143 + # SSLv2 not supported + #$IO::Socket::SSL::DEBUG = 4 ; + $acc->{ sslargs } = { + SSL_version => 'SSLv2', + } ; + + is( + undef, $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 1, + 1, 100, $acc, {}, + ), 'tests_login_imap_starttls: test1.lamiral.info test1 tls SSLv2 not supported' + ) ; + #SSL_verify_mode => 1 + #SSL_version => 'TLSv1_1' + is( undef, $acc->{ imap }, "tests_login_imap_starttls: acc->{ imap } test1 tls error => undef") ; + + + # No name resolution + my $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host2' ; + $acc->{ authmech } = 'LOGIN' ; + is( + undef, login_imap( 'noresol.lamiral.info', 143, 'test1', 'secret1', + 0, 1, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_starttls: noresol.lamiral.info undef' + ) ; + + is( 'ERR_CONNECTION_FAILURE_HOST2', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_starttls: Host2 noresol.lamiral.info => ERR_CONNECTION_FAILURE_HOST2' ) ; + is( undef, $acc->{ imap }, "tests_login_imap_starttls: acc->{ imap } noresol error => undef") ; + } + } + + note( 'Leaving tests_login_imap_starttls()' ) ; + return ; +} + + + + + +sub tests_login_imap_ssl_993 +{ + note( 'Entering tests_login_imap_ssl_993()' ) ; + + SKIP: + { + if ( skip_macosx( ) ) + { + skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 3 ) ; + } + else + { + + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + $acc->{ debugimap } = 1 ; + + + # SSL mode port 993 + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'tests_login_imap_ssl_993: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_ssl_993: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + is( $myimap, $acc->{ imap }, "tests_login_imap_ssl_993: acc->{ imap } ok test1 ssl") ; + } + + note( 'Leaving tests_login_imap_ssl_993()' ) ; + } + return ; +} + + + sub oauthgenerateaccess { if ( "petite" eq hostname() ) { myprint( "oauthgenerateaccess\n" ) ; - my @output = backtick( 'cd oauth2 && pwd && ./generate_gmail_token imapsync.gl0@gmail.com' ) ; + my @output = backtick( 'cd oauth2/oauth2_gmail/ && pwd && ./generate_gmail_token imapsync.gl0@gmail.com' ) ; myprint( @output ) ; } return ; @@ -7884,8 +9017,8 @@ sub oauthgenerateaccess sub tests_login_imap_oauth { note( 'Entering tests_login_imap_oauth()' ) ; - - oauthgenerateaccess() ; + + oauthgenerateaccess( ) ; SKIP: { if ( skip_macosx_binary( ) ) @@ -7901,129 +9034,212 @@ sub tests_login_imap_oauth $mysync = {} ; $acc = {} ; $acc->{ oauthdirect } = 'caca2' ; - $acc->{ debugimap } = 1 ; + #$acc->{ debugimap } = 1 ; $mysync->{ showpasswords } = 1 ; $acc->{ Side } = 'Host2' ; $acc->{ authmech } = 'QQQ' ; is( - undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', - 1, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: user2 bad oauthdirect => undef' ) ; + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', + 1, undef, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_oauth: user2 bad oauthdirect => undef' + ) ; - is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER2' ) ; + is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_oauth: Host2 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER2' ) ; # oauthdirect authentication failure for user1 $mysync = {} ; $acc = {} ; $acc->{ Side } = 'Host1' ; $acc->{ oauthdirect } = 'caca1' ; - $acc->{ debugimap } = 1 ; + #$acc->{ debugimap } = 1 ; $mysync->{ showpasswords } = 1 ; $acc->{ authmech } = 'QQQ' ; is( - undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', - 1, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: user1 bad oauthdirect => undef' ) ; + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', + 1, undef, + 1, 100, $acc, $mysync, + ), 'tests_login_imap_oauth: user1 bad oauthdirect => undef' ) ; - is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER1' ) ; + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_oauth: Host1 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER1' ) ; # oauthdirect authentication failure for user1 $mysync = {} ; $acc = {} ; $acc->{ Side } = 'Host1' ; $acc->{ oauthdirect } = '' ; - $acc->{ debugimap } = 1 ; + #$acc->{ debugimap } = 1 ; $mysync->{ showpasswords } = 1 ; $acc->{ authmech } = 'QQQ' ; is( - undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', - 1, undef, - 1, 100, $acc, $mysync, - ), 'login_imap: user1 bad oauthdirect => undef' ) ; - - is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 no oauthdirect value => ERR_AUTHENTICATION_FAILURE_USER1' ) ; - - } - } - - # oauthdirect authentication success for user1 - SKIP: { - if ( ! -r 'oauth2/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ) - { - skip( 'oauthdirect: no oauthdirect file', 6 ) ; - } - my $myimap ; - my $mysync = {} ; - my $acc = {} ; - $acc->{ Side } = 'Host1' ; - $acc->{ oauthdirect } = 'oauth2/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ; - $acc->{ debugimap } = 1 ; - $mysync->{ showpasswords } = 1 ; - $acc->{ authmech } = 'QQQ' ; - isa_ok( - $myimap = login_imap( 'imap.gmail.com', 993, 'user_useless', 'password_useless', + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', 1, undef, 1, 100, $acc, $mysync, - ), 'Mail::IMAPClient', 'login_imap: user1 good oauthdirect => Mail::IMAPClient' ) ; + ), 'tests_login_imap_oauth: user1 bad oauthdirect => undef' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect IsAuthenticated' ) ; + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'tests_login_imap_oauth: Host1 no oauthdirect value => ERR_AUTHENTICATION_FAILURE_USER1' ) ; - ok( defined( $myimap ) && $myimap->logout( ), 'login_imap: gmail oauth2 oauthdirect logout' ) ; - ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect not IsAuthenticated after logout' ) ; - ok( defined( $myimap ) && $myimap->reconnect( ), 'login_imap: gmail oauth2 oauthdirect reconnect ok' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect IsAuthenticated after reconnect' ) ; + } } - - - # oauthaccesstoken authentication success for user1 + # oauthdirect authentication success for user1 SKIP: { - if ( ! -r 'oauth2/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ) + if ( ! -r 'oauth2/oauth2_gmail/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ) { - skip( 'oauthaccesstoken: no access_token file', 6 ) ; + skip( 'oauthdirect: no oauthdirect file', 21 ) ; } my $myimap ; my $mysync = {} ; my $acc = {} ; $acc->{ Side } = 'Host1' ; - $acc->{ oauthaccesstoken } = 'oauth2/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ; - $acc->{ debugimap } = 1 ; + $acc->{ oauthdirect } = 'oauth2/oauth2_gmail/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ; + #$acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ authmech } = 'QQQ' ; + note( 'login_imap' ) ; + isa_ok( + $myimap = login_imap( 'imap.gmail.com', 993, 'user_useless', 'password_useless', + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'tests_login_imap_oauth: user1 good oauthdirect => Mail::IMAPClient' + ) ; + + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect IsAuthenticated' ) ; + note( 'logout' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect logout 1' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect not IsAuthenticated after logout 1' ) ; + note( 'reconnect' ) ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect reconnect 1 ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect IsAuthenticated after reconnect 1' ) ; + note( 'logout' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect logout 2' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect not IsAuthenticated after logout 2' ) ; + + # failure + $acc->{ oauthdirect } = 'caca3' ; + note( 'reconnect' ) ; + ok( defined( $myimap ) && ! $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect reconnect 2 nok' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect not IsAuthenticated after reconnect 2' ) ; + + # success + $acc->{ oauthdirect } = 'oauth2/oauth2_gmail/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ; + note( 'reconnect' ) ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect reconnect 3 ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect IsAuthenticated after reconnect 3' ) ; + note( 'logout' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect logout 3' ) ; + + # No fake user, no fake password + note( 'login_imap' ) ; + isa_ok( + $myimap = login_imap( 'imap.gmail.com', 993, undef, undef, + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'tests_login_imap_oauth: user1 good oauthdirect no password => Mail::IMAPClient' + ) ; + + #$myimap->Debug( 1 ) ; + + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect no password IsAuthenticated' ) ; + note( 'logout' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect no password logout 1' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect no password not IsAuthenticated after logout 1' ) ; + note( 'reconnect' ) ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect reconnect 1 ok' ) ; + note( 'IsAuthenticated' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect no password IsAuthenticated after reconnect 1' ) ; + + note( 'logout' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect logout 2' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthdirect no password not IsAuthenticated after logout 2' ) ; + } + + # oauthaccesstoken authentication success for user1 + SKIP: { + if ( ! -r 'oauth2/oauth2_gmail/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ) + { + skip( 'oauthaccesstoken: no access_token file', 20 ) ; + } + my $myimap ; + my $mysync = {} ; + my $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ oauthaccesstoken } = 'oauth2/oauth2_gmail/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ; + #$acc->{ debugimap } = 1 ; $mysync->{ showpasswords } = 1 ; $acc->{ authmech } = 'QQQ' ; isa_ok( $myimap = login_imap( 'imap.gmail.com', 993, 'imapsync.gl0@gmail.com', 'password_useless', - 1, undef, - 1, 100, $acc, $mysync, - ), 'Mail::IMAPClient', 'login_imap: user1 good oauthaccesstoken => Mail::IMAPClient' ) ; + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'tests_login_imap_oauth: user1 good oauthaccesstoken => Mail::IMAPClient' + ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken IsAuthenticated' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken IsAuthenticated' ) ; + + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken logout 1' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken not IsAuthenticated after logout 1' ) ; + + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken reconnect 1 ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken IsAuthenticated after reconnect 1' ) ; - ok( defined( $myimap ) && $myimap->logout( ), 'login_imap: gmail oauth2 oauthaccesstoken logout' ) ; - ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken not IsAuthenticated after logout' ) ; - ok( defined( $myimap ) && $myimap->reconnect( ), 'login_imap: gmail oauth2 oauthaccesstoken reconnect ok' ) ; - ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken IsAuthenticated after reconnect' ) ; + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken logout 2' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken not IsAuthenticated after logout 2' ) ; + + # failure + $acc->{ oauthaccesstoken } = 'caca4' ; + ok( defined( $myimap ) && ! $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken reconnect 2 nok' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken not IsAuthenticated after reconnect 2' ) ; + + # success + $acc->{ oauthaccesstoken } = 'oauth2/oauth2_gmail/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken reconnect 3 ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken IsAuthenticated after reconnect 3' ) ; + + # No fake password + isa_ok( + $myimap = login_imap( 'imap.gmail.com', 993, 'imapsync.gl0@gmail.com', undef, + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'tests_login_imap_oauth: user1 good oauthaccesstoken no password => Mail::IMAPClient' + ) ; + + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken no password IsAuthenticated' ) ; + + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken logout 1' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken no password not IsAuthenticated after logout 1' ) ; + + ok( defined( $myimap ) && $myimap->reconnect( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken no password reconnect 1 ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken no password IsAuthenticated after reconnect 1' ) ; + + ok( defined( $myimap ) && $myimap->logout( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken logout 2' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'tests_login_imap_oauth: gmail oauth2 oauthaccesstoken no password not IsAuthenticated after logout 2' ) ; + } - - + + note( 'Leaving tests_login_imap_oauth()' ) ; return ; } - sub login_imap { - my @allargs = @_ ; + my @allargs = @ARG ; my( $host, $port, $user, $password, $ssl, $tls, - $uid, $split, $acc, $mysync ) = @allargs ; + $uid, $split, $acc, $mysync + ) = @allargs ; $acc->{ imap } = undef ; - - if ( ! all_defined( $host, $port, $user, $acc->{ Side } ) ) + + if ( ! all_defined( $host, $port, $acc->{ Side } ) ) + { + return ; + } + + if ( $acc->{ justconnectdone } ) { return ; } @@ -8035,25 +9251,29 @@ sub login_imap if ( ! $imap->connect() ) { - my $error = "$acc->{ Side } failure: can not open imap connection on $side [$host] with user [$user]: " + my $error = "$acc->{ Side } failure: can not open imap connection on $side [$host] port [$port] with user [$user]: " . $imap->LastError . " $OS_ERROR\n" ; errors_incr( $mysync, $error ) ; return ; } - myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), "\n" ) ; + + # Add also $imap->Socket->sockhost() to help configuring firewalls, allowed rule. + myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), " Local IP address: ", $imap->Socket->sockhost(), "\n" ) ; my $banner = $imap->Results()->[0] ; myprint( "$acc->{ Side } banner: $banner" ) ; myprint( "$acc->{ Side } capability before authentication: ", join(q{ }, @{ $imap->capability() || [] }), "\n" ) ; - if ( (! $ssl) and (! defined $tls ) and $imap->has_capability( 'STARTTLS' ) ) { + if ( ( ! $ssl ) and ( ! defined $tls ) and $imap->has_capability( 'STARTTLS' ) ) + { myprint( "$acc->{ Side }: going to ssl because STARTTLS is in CAPABILITY. Use --notls1 or --notls2 to avoid that behavior\n" ) ; $tls = 1 ; } #myprint( Data::Dumper->Dump( [ @allargs ] ) ) ; - if ( $tls ) { + if ( $tls ) + { set_tls( $imap, $acc ) ; if ( ! $imap->starttls( ) ) @@ -8067,12 +9287,16 @@ sub login_imap myprint( "$acc->{ Side }: Socket successfully converted to SSL\n" ) ; } - if ( $acc->{ authmech } eq 'PREAUTH' ) { - if ( $imap->IsAuthenticated( ) ) { + if ( $acc->{ authmech } eq 'PREAUTH' ) + { + if ( $imap->IsAuthenticated( ) ) + { $imap->Socket ; myprintf("%s: Assuming PREAUTH for %s\n", $acc->{ Side }, $imap->Server ) ; - }else{ - $mysync->{nb_errors}++ ; + } + else + { + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_AUTHENTICATION_FAILURE, "$acc->{ Side } failure: error login on $side [$host] with user [$user] auth [PREAUTH]\n" @@ -8103,18 +9327,15 @@ sub init_imap my( $host, $port, $user, $password, $ssl, $tls, - $uid, $split, $acc, $mysync ) = @_ ; + $uid, $split, $acc, $mysync ) = @ARG ; my ( $imap ) ; $imap = Mail::IMAPClient->new() ; - if ( $mysync->{ tee } ) - { - # Well, it does not change anything, does it? - # It does when suppressing the hack with *STDERR - $imap->Debug_fh( $mysync->{ tee } ) ; - } + # Well, it does not change anything, does it? + # It does when suppressing the hack with *STDERR + $imap->Debug_fh( $mysync->{ tee } || *STDOUT ) ; if ( $ssl ) { set_ssl( $imap, $acc ) } if ( $tls ) { } # can not do set_tls() here because connect() will directly do a STARTTLS @@ -8145,7 +9366,7 @@ sub init_imap { $imap->Reconnectretry( $acc->{ reconnectretry } ) ; } - + $imap->{IMAPSYNC_RECONNECT_COUNT} = 0 ; $imap->Ignoresizeerrors( $allowsizemismatch ) ; $split and $imap->Maxcommandlength( $SPLIT_FACTOR * $split ) ; @@ -8160,7 +9381,7 @@ sub authenticate_imap my( $imap, $host, $port, $user, $password, $ssl, $tls, - $uid, $split, $acc, $mysync ) = @_ ; + $uid, $split, $acc, $mysync ) = @ARG ; check_capability( $imap, $acc->{ authmech }, $acc->{ Side } ) ; $imap->User( $user ) ; @@ -8186,15 +9407,15 @@ sub authenticate_imap $acc->{ authmech } = 'XOAUTH2 direct' ; return( oauthdirect( $mysync, $acc, $imap, $host, $user ) ) ; } - - + + if ( defined $acc->{ oauthaccesstoken } ) { $acc->{ authmech } = 'XOAUTH2 accesstoken' ; return( oauthaccesstoken( $mysync, $acc, $imap, $host, $user ) ) ; } - - + + if ( $acc->{ proxyauth } ) { @@ -8281,25 +9502,36 @@ sub failure_proxyauth +sub token +{ + my $oauthtoken = shift @ARG ; + my $oauthrefreshcmd = shift @ARG ; + + if ( defined $oauthrefreshcmd ) + { + myprint( "Executing script: $oauthrefreshcmd\n" ) ; + my $output = backtick( "$oauthrefreshcmd" ) ; + if ( $output ) + { + print "Output of $oauthrefreshcmd\n$output" ; + } + } + + my $token = firstline_or_string( $oauthtoken ) ; + + return( $token ) ; + +} sub oauthdirect { - my( $mysync, $acc, $imap, $host, $user ) = @_ ; - - my $oauthdirect_str ; - if ( -f -r $acc->{ oauthdirect } ) - { - $oauthdirect_str = firstline( $acc->{ oauthdirect } ) ; - } - else - { - $oauthdirect_str = $acc->{ oauthdirect } || 'Please define oauthdirect value' ; - } + my( $mysync, $acc, $imap, $host, $user ) = @ARG ; $imap->Authmechanism( 'XOAUTH2' ) ; - $imap->Authcallback( sub { return $oauthdirect_str } ) ; - - #if ( $imap->authenticate('XOAUTH2', sub { return $oauthdirect_str } ) ) + defined( $imap->User ) || $imap->User ( 'FakeUser ' ) ; + defined( $imap->Password ) || $imap->Password( 'FakePassword' ) ; + $imap->Authcallback( sub { return( token( $mysync, $acc->{ oauthdirect }, $acc->{ oauthrefreshcmd } ) ) } ) ; + if ( $imap->login( ) ) { return 1 ; @@ -8313,34 +9545,58 @@ sub oauthdirect } +sub tests_oauth2_bearer_string +{ + note( 'Entering tests_oauth2_bearer_string()' ) ; + + is( undef, oauth2_bearer_string( ), 'tests_oauth2_bearer_string: no args => undef' ) ; + + is( undef, oauth2_bearer_string( '' ), 'tests_oauth2_bearer_string: empty string, no user => undef' ) ; + + my $mysync = { } ; + + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '' ), 'tests_oauth2_bearer_string: empty string, empty user => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '', 'noexists' ), 'tests_oauth2_bearer_string: empty string, empty user, noexists command => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '', 'true' ), 'tests_oauth2_bearer_string: empty string, empty user, true command => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '', 'false' ), 'tests_oauth2_bearer_string: empty string, empty user, false command => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '', 'ls /kkkk' ), 'tests_oauth2_bearer_string: empty string, empty user, ls /kkkk command => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + + is( 'dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ==', oauth2_bearer_string( $mysync, '', '', 'echo Hi!' ), 'tests_oauth2_bearer_string: empty string, empty user, empty command => dXNlcj0BYXV0aD1CZWFyZXIgcGxlYXNlLCBnaXZlIGEgdmFsdWUBAQ' ) ; + + note( 'Leaving tests_oauth2_bearer_string()' ) ; + return ; +} + +sub oauth2_bearer_string +{ + + my $mysync = shift @ARG ; + my $oauthaccesstoken = shift @ARG ; + my $user = shift @ARG ; + my $oauthrefreshcmd = shift @ARG ; + + if ( ! defined $mysync ) { return ; } + if ( ! defined $oauthaccesstoken ) { return ; } + if ( ! defined $user ) { return ; } + + my $oauthaccesstoken_str = token( $oauthaccesstoken, $oauthrefreshcmd ) ; + my $oauth_string = "user=" . $user . "\x01auth=Bearer ". $oauthaccesstoken_str . "\x01\x01" ; + + my $oauthdirect_str = encode_base64( $oauth_string , '' ) ; + + return $oauthdirect_str ; +} sub oauthaccesstoken { - my( $mysync, $acc, $imap, $host, $user ) = @_ ; - - my $oauthaccesstoken_str ; - if ( -f -r $acc->{ oauthaccesstoken } ) - { - $oauthaccesstoken_str = firstline( $acc->{ oauthaccesstoken } ) ; - } - else - { - $oauthaccesstoken_str = $acc->{ oauthaccesstoken } || 'Please define oauthaccesstoken value' ; - } - - my $oauth_string = "user=" . $user . "\x01auth=Bearer ". $oauthaccesstoken_str . "\x01\x01" ; - #myprint "oauth_string: $oauth_string\n" ; - - my $oauth_string_base64 = encode_base64( $oauth_string , '' ) ; - #myprint "oauth_string_base64: $oauth_string_base64\n" ; - - my $oauthdirect_str = $oauth_string_base64 ; + my( $mysync, $acc, $imap, $host, $user ) = @ARG ; $imap->Authmechanism( 'XOAUTH2' ) ; - $imap->Authcallback( sub { return $oauthdirect_str } ) ; - - #if ( $imap->authenticate('XOAUTH2', sub { return $oauthdirect_str } ) ) + defined( $imap->Password ) || $imap->Password( 'FakePassword' ) ; + $imap->Authcallback( sub { return oauth2_bearer_string( $mysync, $acc->{ oauthaccesstoken }, $user, $acc->{ oauthrefreshcmd } ) } ) ; + if ( $imap->login( ) ) { return 1 ; @@ -8359,7 +9615,7 @@ sub oauthaccesstoken sub check_capability { - my( $imap, $authmech, $Side ) = @_ ; + my( $imap, $authmech, $Side ) = @ARG ; if ( $imap->has_capability( "AUTH=$authmech" ) @@ -8391,7 +9647,7 @@ sub check_capability sub set_ssl { - my ( $imap, $acc ) = @_ ; + my ( $imap, $acc ) = @ARG ; # SSL_version can be # SSLv3 SSLv2 SSLv23 SSLv23:!SSLv2 (last one is the default in IO-Socket-SSL-1.953) # @@ -8421,7 +9677,7 @@ sub set_ssl sub set_tls { - my ( $imap, $acc ) = @_ ; + my ( $imap, $acc ) = @ARG ; my $sslargs_hash = $acc->{sslargs} ; @@ -8492,15 +9748,15 @@ sub xoauth2 require HTML::Entities ; require JSON ; require JSON::WebToken::Crypt::RSA ; - require Crypt::OpenSSL::PKCS12; require Crypt::OpenSSL::RSA ; + require Crypt::OpenSSL::PKCS12 ; require Encode::Byte ; require IO::Socket::SSL ; my $code = shift; my $imap = shift; - my ($iss,$key); + my ( $iss, $key ) ; if( $imap->Password =~ /^(.*\.json)$/x ) { @@ -8510,7 +9766,7 @@ sub xoauth2 my $FILE ; if ( ! open( $FILE, '<', $filename ) ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "error [$filename]: $OS_ERROR\n" ) ; @@ -8518,10 +9774,10 @@ sub xoauth2 my $jsonfile = $json->decode( join q{}, <$FILE> ) ; close $FILE ; - $iss = $jsonfile->{client_id}; - $key = $jsonfile->{private_key}; - $sync->{ debug } and myprint( "Service account: $iss\n"); - $sync->{ debug } and myprint( "Private key:\n$key\n"); + $iss = $jsonfile->{ client_id } ; + $key = $jsonfile->{ private_key } ; + $sync->{ debug } and myprint( "Service account: $iss\n" ) ; + $sync->{ debug } and myprint( "Private key:\n$key\n" ) ; } else { @@ -8529,20 +9785,25 @@ sub xoauth2 ( $iss, my $keyfile, my $keypass ) = $imap->Password =~ /([\-\d\w\@\.]+);([a-zA-Z0-9 \_\-\.\/]+);?(.*)?/x ; # Assume key password is google default if not provided - $keypass = 'notasecret' if not $keypass; + $keypass = 'notasecret' if not $keypass ; - $sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n"); + $sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n") ; - # Get private key from p12 file - my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile); - $key = $pkcs12->private_key($keypass); + # Get private key from p12 file (would be better in perl...) + # $key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver` ; + + # https://github.com/imapsync/imapsync/pull/381/files + # Get private key from p12 file + my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file( $keyfile ) ; + $key = $pkcs12->private_key( $keypass ) ; - $sync->{ debug } and myprint( "Private key:\n$key\n"); + $sync->{ debug } and myprint( "Private key:\n$key\n") ; } # Create jwt of oauth2 request my $time = time ; - my $jwt = JSON::WebToken->encode( { + my $jwt = JSON::WebToken->encode( + { 'iss' => $iss, # service account 'scope' => 'https://mail.google.com/', 'aud' => 'https://www.googleapis.com/oauth2/v3/token', @@ -8560,12 +9821,15 @@ sub xoauth2 { grant_type => HTML::Entities::encode_entities('urn:ietf:params:oauth:grant-type:jwt-bearer'), assertion => $jwt } ) ; - unless( $response->is_success( ) ) { + unless( $response->is_success( ) ) + { $sync->{nb_errors}++ ; exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, $response->code, "\n", $response->content, "\n" ) ; - }else{ + } + else + { $sync->{ debug } and myprint( $response->content ) ; } @@ -8575,20 +9839,20 @@ sub xoauth2 # format as oauth2 auth data my $xoauth2_string = encode_base64( 'user=' . $imap->User . "\1auth=Bearer " . $data->{access_token} . "\1\1", q{} ) ; - $sync->{ debug } and myprint( "XOAUTH2 String: $xoauth2_string\n"); - return($xoauth2_string); -} + $sync->{ debug } and myprint( "XOAUTH2 String: $xoauth2_string\n" ) ; + return( $xoauth2_string ) ; +} -sub xmasterauth +sub xmasterauth { # This is Kerio auth admin # This code comes from # https://github.com/imapsync/imapsync/pull/53/files - my $imap = shift ; + my $imap = shift @ARG ; my $user = $imap->User( ) ; my $password = $imap->Password( ) ; @@ -8597,7 +9861,7 @@ sub xmasterauth my @challenge = $imap->tag_and_run( $authmech, "+" ) ; if ( not defined $challenge[0] ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", $imap->LastError, "\n" @@ -8609,7 +9873,7 @@ sub xmasterauth $challenge[1] =~ s/^\+ |^\s+|\s+$//g ; if ( ! $imap->_imap_command( { addcrlf => 1, addtag => 0, tag => $imap->Count }, md5_hex( $challenge[1] . $password ) ) ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", $imap->LastError, "\n" @@ -8618,7 +9882,7 @@ sub xmasterauth if ( ! $imap->tag_and_run( 'X-SETUSER ' . $user ) ) { - $sync->{nb_errors}++ ; + $sync->{ nb_errors }++ ; exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", "X-SETUSER ", $imap->LastError, "\n" @@ -8630,14 +9894,14 @@ sub xmasterauth # $imap->State( Mail::IMAPClient::Selected ) ; return ; -} +} sub keepalive1 { - my $mysync = shift ; - + my $mysync = shift @ARG ; + $mysync->{ acc1 }->{ keepalive } = defined $mysync->{ acc1 }->{ keepalive } ? $mysync->{ acc1 }->{ keepalive } : 1 ; - + if ( $mysync->{ acc1 }->{ keepalive } ) { myprint( "Host1: imap connection keepalive is on on host1. Use --nokeepalive1 to disable it.\n" ) ; @@ -8646,14 +9910,16 @@ sub keepalive1 { myprint( "Host1: imap connection keepalive is off on host1. Use --keepalive1 to enable it.\n" ) ; } + + return ; } sub keepalive2 { - my $mysync = shift ; - + my $mysync = shift @ARG ; + $mysync->{ acc2 }->{ keepalive } = defined $mysync->{ acc2 }->{ keepalive } ? $mysync->{ acc2 }->{ keepalive } : 1 ; - + if ( $mysync->{ acc2 }->{ keepalive } ) { myprint( "Host2: imap connection keepalive is on on host2. Use --nokeepalive2 to disable it.\n" ) ; @@ -8662,6 +9928,8 @@ sub keepalive2 { myprint( "Host2: imap connection keepalive is off on host2. Use --keepalive2 to enable it.\n" ) ; } + + return ; } @@ -8673,8 +9941,8 @@ sub banner_imapsync my $banner_imapsync = join q{}, q{$RCSfile: imapsync,v $ }, - q{$Revision: 2.178 $ }, - q{$Date: 2022/01/12 21:28:37 $ }, + q{$Revision: 2.314 $ }, + q{$Date: 2025/09/23 16:55:17 $ }, "\n", "Command line used, run by $EXECUTABLE_NAME:\n", "$PROGRAM_NAME ", command_line_nopassword( $mysync, @argv ), "\n" ; @@ -8682,57 +9950,139 @@ sub banner_imapsync return( $banner_imapsync ) ; } -sub tests_do_valid_directory -{ - note( 'Entering tests_do_valid_directory()' ) ; +sub tests_make_valid_directory +{ + note( 'Entering tests_make_valid_directory()' ) ; - is( 1, do_valid_directory( '.'), 'do_valid_directory: . good' ) ; - is( 1, do_valid_directory( './W/tmp/tests/valid/sub'), 'do_valid_directory: ./W/tmp/tests/valid/sub good' ) ; + is( undef, make_valid_directory( ), 'tests_make_valid_directory: no args => undef' ) ; + + is( undef, make_valid_directory( '' ), 'tests_make_valid_directory: empty string => undef' ) ; + + my $mysync = { } ; + is( undef, make_valid_directory( $mysync ), 'tests_make_valid_directory: no dir => undef' ) ; + + + # Cite a common file on any system: myself. Dangerous? + is( 0, make_valid_directory( $mysync, $0 ), 'tests_make_valid_directory: a file is not a valid directory' ) ; + # + is( 1, make_valid_directory( $mysync, '.' ), 'tests_make_valid_directory: . good' ) ; + is( 1, make_valid_directory( $mysync, './W/tmp/tests/valid/sub' ), 'tests_make_valid_directory: ./W/tmp/tests/valid/sub ok' ) ; + is( 1, make_valid_directory( $mysync, './W/tmp/tests/valid' ), 'tests_make_valid_directory: ./W/tmp/tests/valid ok' ) ; + is( 1, make_valid_directory( $mysync, './W/tmp/tests/valid/sub' ), 'tests_make_valid_directory: ./W/tmp/tests/valid/sub ok 2' ) ; + is( 1, make_valid_directory( $mysync, './W/tmp/tests/valid/qqq' ), 'tests_make_valid_directory: ./W/tmp/tests/valid/qqq ok' ) ; - Readonly my $NB_UNIX_tests_do_valid_directory_non_root => 2 ; diag( "OSNAME=$OSNAME EFFECTIVE_USER_ID=$EFFECTIVE_USER_ID" ) ; - SKIP: { - skip( 'Tests only for non roor user', $NB_UNIX_tests_do_valid_directory_non_root ) if ( '0' eq $EFFECTIVE_USER_ID ) ; - diag( 'The "Error / is not writable" is on purpose' ) ; - ok( 0 == do_valid_directory( '/'), 'do_valid_directory: / bad' ) ; + SKIP: + { + if ( '0' eq $EFFECTIVE_USER_ID ) + { + skip( 'Tests only for non root user', 2 ) + } + diag( 'The "Error / has not good enough permissions" is on purpose' ) ; + ok( 0 == make_valid_directory( $mysync, '/'), 'tests_make_valid_directory: / is not valid for me' ) ; diag( 'The "Error permission denied" on /noway is on purpose' ) ; - ok( 0 == do_valid_directory( '/noway'), 'do_valid_directory: /noway bad' ) ; + ok( 0 == make_valid_directory( $mysync, '/noway'), 'tests_make_valid_directory: /noway is not valid for me' ) ; } + SKIP: + { + if ( 'MSWin32' ne $OSNAME ) + { + skip( 'Tests only for MSWin32', 1 ) + } + diag( 'The error mkdir K:\Beurk is on purpose' ) ; + ok( 0 == make_valid_directory( $mysync, 'K:\Beurk' ), 'tests_make_valid_directory: K:\Beurk failure but that is what we want' ) ; + } - note( 'Leaving tests_do_valid_directory()' ) ; + note( 'Dump of the buffered output' ) ; + myprint( output( $mysync ) ) ; + + note( 'Leaving tests_make_valid_directory()' ) ; return ; -} +} -sub do_valid_directory + + +sub make_valid_directory { - my $dir = shift @ARG ; - # all good => return ok. - return( 1 ) if ( -d $dir and -r _ and -w _ ) ; + my $mysync = shift @ARG ; + my $dir = shift @ARG ; + + if ( ! defined( $mysync ) ) { return ; } + if ( ! defined( $dir ) ) { return ; } + if ( ! $dir ) { return ; } # exist but bad - if ( -e $dir and not -d _ ) { - myprint( "Error: $dir exists but is not a directory\n" ) ; + if ( -e $dir and not -d _ ) + { + output( $mysync, "Error: $dir exists but it is not a directory\n" ) ; return( 0 ) ; } - if ( -e $dir and not -w _ ) { - my $sb = stat $dir ; - myprintf( "Error: directory %s is not writable for user %s, permissions are %04o and owner is %s ( uid %s )\n", - $dir, getpwuid_any_os( $EFFECTIVE_USER_ID ), ($sb->mode & oct($PERMISSION_FILTER) ), getpwuid_any_os( $sb->uid ), $sb->uid( ) ) ; - return( 0 ) ; + + if ( not -e $dir ) + { + # Trying to create it + output( $mysync, "Creating directory $dir (current directory is " . getcwd( ) . ")\n" ) ; + if ( ! eval { mkpath( $dir ) } ) + { + if ( $EVAL_ERROR ) + { + output( $mysync, "$EVAL_ERROR" ) ; + } + return( 0 ) ; + } } - # Trying to create it - myprint( "Creating directory $dir (current directory is " . getcwd( ) . ")\n" ) ; - if ( ! eval { mkpath( $dir ) } ) { - myprint( "$EVAL_ERROR" ) if ( $EVAL_ERROR ) ; - } - return( 1 ) if ( -d $dir and -r _ and -w _ ) ; - return( 0 ) ; + + return( is_dir_writable_or_make_it_writable( $mysync, $dir ) ) ; } +sub is_dir_writable_or_make_it_writable +{ + my $mysync = shift @ARG ; + my $dir = shift @ARG ; + + if ( -e $dir and not -d _ ) + { + # exist but bad + output( $mysync, "Error: $dir exists but it is not a directory\n" ) ; + return( 0 ) ; + } + elsif ( -d $dir and -r _ and -w _ and -x _ ) + { + # all good => return ok. + output( $mysync, "Directory $dir is writable and readable and gointoable. Good.\n" ) ; + return( 1 ) ; + } + elsif ( -e $dir ) + { + # not writable or not readable or not gointoable + output( $mysync, "Directory $dir has not good enough permissions. Trying a chmod 755.\n" ) ; + if ( chmod( 0755, $dir ) and ( -d $dir and -r _ and -w _ ) ) + { + output( $mysync, "Directory $dir is now writable and readable. Good.\n" ) ; + return( 1 ) ; + } + else + { + my $sb = stat $dir ; + output( $mysync, + sprintf( "Bad: directory %s for user %s, permissions are %04o and owner is %s ( uid %s )\n", + $dir, getpwuid_any_os( $EFFECTIVE_USER_ID ), ($sb->mode & oct($PERMISSION_FILTER) ), + getpwuid_any_os( $sb->uid ), $sb->uid( ) ) ) ; + return( 0 ) ; + } + } + else + { + # sounds dark here. + output( $mysync, "Directory $dir does not exist\n" ) ; + return( 0 ) ; + } +} + sub tests_match_a_pid_number { note( 'Entering tests_match_a_pid_number()' ) ; @@ -8783,6 +10133,7 @@ sub tests_remove_pidfile_not_running note( 'Entering tests_remove_pidfile_not_running()' ) ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'remove_pidfile_not_running: mkpath W/tmp/tests/' ) ; + is( undef, remove_pidfile_not_running( ), 'remove_pidfile_not_running: no args => undef' ) ; is( undef, remove_pidfile_not_running( './W' ), 'remove_pidfile_not_running: a dir => undef' ) ; is( undef, remove_pidfile_not_running( 'noexists' ), 'remove_pidfile_not_running: noexists => undef' ) ; @@ -8803,16 +10154,16 @@ sub remove_pidfile_not_running { # my $pid_filename = shift @ARG ; - + #myprint( "In remove_pidfile_not_running $pid_filename\n" ) ; if ( ! $pid_filename ) { myprint( "No variable pid_filename\n" ) ; return } ; if ( ! -e $pid_filename ) - { + { myprint( "File $pid_filename does not exist\n" ) ; return ; } #myprint( "Still In remove_pidfile_not_running $pid_filename\n" ) ; - + if ( ! -f $pid_filename ) { myprint( "File $pid_filename is not a file\n" ) ; return } ; my $pid = firstline( $pid_filename ) ; @@ -8836,8 +10187,8 @@ sub remove_pidfile_not_running } -sub tests_tail -{ +sub tests_tail +{ note( 'Entering tests_tail()' ) ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'tail: mkpath W/tmp/tests/' ) ; @@ -8848,15 +10199,15 @@ sub tests_tail my $mysync ; is( undef, tail( $mysync ), 'tail: no pidfile => undef' ) ; - $mysync->{pidfile} = 'W/tmp/tests/tail.pid' ; + $mysync->{ pidfile } = 'W/tmp/tests/tail.pid' ; is( undef, tail( $mysync ), 'tail: no pidfilelocking => undef' ) ; - $mysync->{pidfilelocking} = 1 ; + $mysync->{ pidfilelocking } = 1 ; is( undef, tail( $mysync ), 'tail: pidfile no exists => undef' ) ; my $pidandlog = "33333\nW/tmp/tests/tail.txt\n" ; - is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put pid 33333 and tail.txt in pidfile' ) ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{ pidfile } ), 'tail: put pid 33333 and tail.txt in pidfile' ) ; is( undef, tail( $mysync ), 'tail: logfile to tail no exists => undef' ) ; my $tailcontent = "L1\nL2\nL3\nL4\nL5\n" ; @@ -8870,46 +10221,59 @@ sub tests_tail # put my own pid, won't do tail $pidandlog = "$PROCESS_ID\nW/tmp/tests/tail.txt\n" ; - is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put my own PID in pidfile' ) ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{ pidfile } ), 'tail: put my own PID in pidfile' ) ; is( undef, tail( $mysync ), 'tail: my own pid in pidfile => undef' ) ; note( 'Leaving tests_tail()' ) ; return ; -} +} -sub tail +sub tail { # return undef on failures # return 1 on success - my $mysync = shift ; + my $mysync = shift @ARG ; # no tail when aborting! if ( $mysync->{ abort } ) { return ; } - my $pidfile = $mysync->{pidfile} ; - my $lock = $mysync->{pidfilelocking} ; - my $tail = $mysync->{tail} ; + my $pidfile = $mysync->{ pidfile } ; + my $lock = $mysync->{ pidfilelocking } ; + my $tail = $mysync->{ tail } ; if ( ! $pidfile ) { return ; } if ( ! $lock ) { return ; } if ( ! $tail ) { return ; } if ( ! -e $pidfile ) { return ; } - + + my $pidtotail = firstline( $pidfile ) ; - if ( ! $pidtotail ) { return ; } + if ( ! $pidtotail ) + { + myprint( "Can not do a tail -f : no pid in 1st line of pidfile $pidfile\n" ) ; + return ; + } # It should not happen but who knows... - if ( $pidtotail eq $PROCESS_ID ) { return ; } + if ( $pidtotail eq $PROCESS_ID ) + { + myprint( "Will not do a tail -f for processus pid $pidtotail : it is myself.\n" ) ; + return ; + } my $filetotail = secondline( $pidfile ) ; - if ( ! $filetotail ) { return ; } + if ( ! $filetotail ) + { + myprint( "Can not do a tail -f for processus pid $pidtotail : no log file in 2nd line of the pidfile $pidfile\n" ) ; + return ; + } if ( ! -r $filetotail ) { @@ -8937,12 +10301,12 @@ sub tail } return 1 ; -} +} sub isrunning { - my $pidtocheck = shift ; - my $moretimes_ref = shift ; + my $pidtocheck = shift @ARG ; + my $moretimes_ref = shift @ARG ; if ( kill 'ZERO', $pidtocheck ) { @@ -8962,39 +10326,43 @@ sub isrunning } } -sub tests_write_pidfile +sub tests_write_pidfile { note( 'Entering tests_write_pidfile()' ) ; my $mysync ; + # return status: + # 1 is okay + # undef is fatal + is( 1, write_pidfile( ), 'write_pidfile: no args => 1' ) ; # no pidfile => ok - $mysync->{pidfile} = q{} ; - is( 1, write_pidfile( $mysync ), 'write_pidfile: no pidfile => undef' ) ; + $mysync->{ pidfile } = q{} ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: no pidfile => 1' ) ; # The pidfile path is bad => failure - $mysync->{pidfile} = '/no/no/no.pid' ; + $mysync->{ pidfile } = '/no/no/no.pid' ; is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid, no lock => undef' ) ; - $mysync->{pidfilelocking} = 1 ; + $mysync->{ pidfilelocking } = 1 ; is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid + lock => undef' ) ; - $mysync->{pidfile} = 'W/tmp/tests/test.pid' ; + $mysync->{ pidfile } = 'W/tmp/tests/test.pid' ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'write_pidfile: mkpath W/tmp/tests/' ) ; - is( 1, touch( $mysync->{pidfile} ), 'write_pidfile: lock prepa' ) ; + is( 1, touch( $mysync->{ pidfile } ), 'write_pidfile: lock prepa' ) ; - $mysync->{pidfilelocking} = 0 ; + $mysync->{ pidfilelocking } = 0 ; is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock => 1' ) ; is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains $PROCESS_ID" ) ; is( q{}, secondline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains no second line" ) ; - $mysync->{pidfilelocking} = 1 ; + $mysync->{ pidfilelocking } = 1 ; is( undef, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + lock => undef' ) ; - $mysync->{pidfilelocking} = 0 ; + $mysync->{ pidfilelocking } = 0 ; $mysync->{ logfile } = 'rrrr.txt' ; is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock + logfile => 1' ) ; is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: + no lock + logfile W/tmp/tests/test.pid contains $PROCESS_ID" ) ; @@ -9003,16 +10371,15 @@ sub tests_write_pidfile note( 'Leaving tests_write_pidfile()' ) ; return ; -} +} -sub write_pidfile +sub write_pidfile { # returns undef if something is considered fatal # returns 1 otherwise - - #myprint( "In write_pidfile\n" ) ; + if ( ! @ARG ) { return 1 ; } my $mysync = shift @ARG ; @@ -9020,7 +10387,6 @@ sub write_pidfile # Do not write the pid file if the current process goal is to abort the process designed by the pid file if ( $mysync->{ abort } ) { return 1 ; } - # my $pid_filename = $mysync->{ pidfile } ; my $lock = $mysync->{ pidfilelocking } ; @@ -9031,13 +10397,15 @@ sub write_pidfile } myprint( "PID file is $pid_filename ( to change it, use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ; - if ( -e $pid_filename and $lock ) { + if ( -e $pid_filename and $lock ) + { myprint( "$pid_filename already exists, another imapsync may be curently running. Aborting imapsync.\n" ) ; return ; } - if ( -e $pid_filename ) { + if ( -e $pid_filename ) + { myprint( "$pid_filename already exists, overwriting it ( use --pidfilelocking to avoid concurrent runs )\n" ) ; } @@ -9050,7 +10418,8 @@ sub write_pidfile $pid_message .= "Writing also my logfile name in $pid_filename : $mysync->{ logfile }\n" ; } - if ( open my $FILE_HANDLE, '>', $pid_filename ) { + if ( open my $FILE_HANDLE, '>', $pid_filename ) + { myprint( $pid_message ) ; print $FILE_HANDLE $pid_string ; close $FILE_HANDLE ; @@ -9061,62 +10430,79 @@ sub write_pidfile myprint( "Could not open $pid_filename for writing. Check permissions or disk space: $OS_ERROR\n" ) ; return ; } -} +} -sub fix_Inbox_INBOX_mapping -{ - my( $h1_all, $h2_all ) = @_ ; - - my $regex = q{} ; - SWITCH: { - if ( exists $h1_all->{INBOX} and exists $h2_all->{INBOX} ) { $regex = q{} ; last SWITCH ; } ; - if ( exists $h1_all->{Inbox} and exists $h2_all->{Inbox} ) { $regex = q{} ; last SWITCH ; } ; - if ( exists $h1_all->{INBOX} and exists $h2_all->{Inbox} ) { $regex = q{s/^INBOX$/Inbox/x} ; last SWITCH ; } ; - if ( exists $h1_all->{Inbox} and exists $h2_all->{INBOX} ) { $regex = q{s/^Inbox$/INBOX/x} ; last SWITCH ; } ; - } ; - return( $regex ) ; -} - sub tests_fix_Inbox_INBOX_mapping { note( 'Entering tests_fix_Inbox_INBOX_mapping()' ) ; - my( $h1_all, $h2_all ) ; + my( $mysync, $h1_all, $h2_all ) ; $h1_all = { 'INBOX' => q{} } ; $h2_all = { 'INBOX' => q{} } ; - ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX INBOX' ) ; + ok( q{} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: INBOX INBOX' ) ; $h1_all = { 'Inbox' => q{} } ; $h2_all = { 'Inbox' => q{} } ; - ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox Inbox' ) ; + ok( q{} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: Inbox Inbox' ) ; $h1_all = { 'INBOX' => q{} } ; $h2_all = { 'Inbox' => q{} } ; - ok( q{s/^INBOX$/Inbox/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX Inbox' ) ; + ok( q{s/^INBOX$/Inbox/x} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: INBOX Inbox' ) ; $h1_all = { 'Inbox' => q{} } ; $h2_all = { 'INBOX' => q{} } ; - ok( q{s/^Inbox$/INBOX/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox INBOX' ) ; + ok( q{s/^Inbox$/INBOX/x} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: Inbox INBOX' ) ; $h1_all = { 'INBOX' => q{} } ; $h2_all = { 'rrrrr' => q{} } ; - ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX rrrrrr' ) ; + ok( q{} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: INBOX rrrrrr' ) ; $h1_all = { 'rrrrr' => q{} } ; $h2_all = { 'Inbox' => q{} } ; - ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: rrrrr Inbox' ) ; + ok( q{} eq fix_Inbox_INBOX_mapping( $mysync, $h1_all, $h2_all ), 'tests_fix_Inbox_INBOX_mapping: rrrrr Inbox' ) ; note( 'Leaving tests_fix_Inbox_INBOX_mapping()' ) ; return ; } +sub fix_Inbox_INBOX_mapping +{ + my( $mysync, $h1_all, $h2_all ) = @ARG ; + + myprint( "Looking at INBOX or Inbox on both sides and deciding of the mapping to apply. Use --nofixInboxINBOX to avoid this behavior.\n" ) ; + my $regex = q{} ; + SWITCH: { + if ( exists $h1_all->{INBOX} and exists $h2_all->{INBOX} ) + { + myprint( "INBOX INBOX on both sides so no mapping applied.\n" ) ; + $regex = q{} ; last SWITCH ; + } ; + if ( exists $h1_all->{Inbox} and exists $h2_all->{Inbox} ) + { + myprint( "Inbox Inbox on both sides so no mapping applied.\n" ) ; + $regex = q{} ; last SWITCH ; + } ; + if ( exists $h1_all->{INBOX} and exists $h2_all->{Inbox} ) + { + myprint( "INBOX Inbox so INBOX -> Inbox mapping applied.\n" ) ; + $regex = q{s/^INBOX$/Inbox/x} ; last SWITCH ; + } ; + if ( exists $h1_all->{Inbox} and exists $h2_all->{INBOX} ) + { + myprint( "Inbox INBOX so Inbox -> INBOX mapping applied.\n" ) ; + $regex = q{s/^Inbox$/INBOX/x} ; last SWITCH ; + } ; + } ; + return( $regex ) ; +} + sub jux_utf8_list { - my @s_inp = @_ ; + my @s_inp = @ARG ; my $s_out = q{} ; foreach my $s ( @s_inp ) { $s_out .= jux_utf8( $s ) . "\n" ; @@ -9132,7 +10518,7 @@ sub tests_jux_utf8_list is( q{}, jux_utf8_list( ), 'jux_utf8_list: void' ) ; is( "[]\n", jux_utf8_list( q{} ), 'jux_utf8_list: empty string' ) ; is( "[INBOX]\n", jux_utf8_list( 'INBOX' ), 'jux_utf8_list: INBOX' ) ; - is( "[&ANY-] = [Ö]\n", jux_utf8_list( '&ANY-' ), 'jux_utf8_list: [&ANY-] = [Ö]' ) ; + is( "[&ANY-] = [Ö]\n", jux_utf8_list( '&ANY-' ), 'jux_utf8_list: [&ANY-] = [Ö]' ) ; note( 'Leaving tests_jux_utf8_list()' ) ; return( 0 ) ; @@ -9147,11 +10533,11 @@ sub tests_jux_utf8_old is( '[]', jux_utf8_old( q{} ), 'jux_utf8_old: void => []' ) ; is( '[INBOX]', jux_utf8_old( 'INBOX'), 'jux_utf8_old: INBOX => [INBOX]' ) ; - is( '[&ZTZO9nux-] = [æ”¶ä»¶ç®±]', jux_utf8_old( '&ZTZO9nux-'), 'jux_utf8_old: => [&ZTZO9nux-] = [æ”¶ä»¶ç®±]' ) ; - is( '[&ANY-] = [Ö]', jux_utf8_old( '&ANY-'), 'jux_utf8_old: &ANY- => [&ANY-] = [Ö]' ) ; + is( '[&ZTZO9nux-] = [收件箱]', jux_utf8_old( '&ZTZO9nux-'), 'jux_utf8_old: => [&ZTZO9nux-] = [收件箱]' ) ; + is( '[&ANY-] = [Ö]', jux_utf8_old( '&ANY-'), 'jux_utf8_old: &ANY- => [&ANY-] = [Ö]' ) ; # +BD8EQAQ1BDQEOwQ+BDM- SHOULD stay as is! - is( '[+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]', jux_utf8_old( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8_old: => [+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' ) ; - is( '[&BB8EQAQ+BDUEOgRC-] = [Проект]', jux_utf8_old( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8_old: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ; + is( '[+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]', jux_utf8_old( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8_old: => [+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' ) ; + is( '[&BB8EQAQ+BDUEOgRC-] = [Проект]', jux_utf8_old( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8_old: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ; note( 'Leaving tests_jux_utf8_old()' ) ; return ; @@ -9160,7 +10546,7 @@ sub tests_jux_utf8_old sub jux_utf8_old { # juxtapose utf8 at the right if different - my ( $s_utf7 ) = shift ; + my ( $s_utf7 ) = shift @ARG ; my ( $s_utf8 ) = imap_utf7_decode_old( $s_utf7 ) ; if ( $s_utf7 eq $s_utf8 ) { @@ -9177,7 +10563,7 @@ sub jux_utf8_old # https://rt.cpan.org/Public/Bug/Display.html?id=11172 sub imap_utf7_decode_old { - my ( $s ) = shift ; + my ( $s ) = shift @ARG ; # Algorithm # On remplace , par / dans les BASE 64 (, entre & et -) @@ -9205,23 +10591,23 @@ sub tests_jux_utf8 # This test is because the binary can fail on it, a PAR.pm issue. # The failure was with the underlying Encode::IMAPUTF7 module line 66 release 1.05 # Was solved by including Encode in imapsync and using "pp -x". - ok( find_encoding( "UTF-16BE"), 'jux_utf8: Encode::find_encoding: UTF-16BE' ) ; + ok( Encode::find_encoding( "UTF-16BE"), 'jux_utf8: Encode::find_encoding: UTF-16BE' ) ; # is( '[]', jux_utf8( q{} ), 'jux_utf8: void => []' ) ; is( '[INBOX]', jux_utf8( 'INBOX'), 'jux_utf8: INBOX => [INBOX]' ) ; - is( '[&ANY-] = [Ö]', jux_utf8( '&ANY-'), 'jux_utf8: &ANY- => [&ANY-] = [Ö]' ) ; + is( '[&ANY-] = [Ö]', jux_utf8( '&ANY-'), 'jux_utf8: &ANY- => [&ANY-] = [Ö]' ) ; # +BD8EQAQ1BDQEOwQ+BDM- must stay as is is( '[+BD8EQAQ1BDQEOwQ+BDM-]', jux_utf8( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8: => [+BD8EQAQ1BDQEOwQ+BDM-] = [+BD8EQAQ1BDQEOwQ+BDM-]' ) ; - is( '[&BB8EQAQ+BDUEOgRC-] = [Проект]', jux_utf8( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ; + is( '[&BB8EQAQ+BDUEOgRC-] = [Проект]', jux_utf8( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ; - is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( q{R&AOk-ponses 1200+1201+1202} ), 'jux_utf8: [R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]' ) ; - my $str = Encode::IMAPUTF7::encode("IMAP-UTF-7", 'Réponses 1200+1201+1202' ) ; - is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( $str ), "jux_utf8: [$str] = [Réponses 1200+1201+1202]" ) ; + is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( q{R&AOk-ponses 1200+1201+1202} ), 'jux_utf8: [R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]' ) ; + my $str = Encode::IMAPUTF7::encode("IMAP-UTF-7", 'Réponses 1200+1201+1202' ) ; + is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( $str ), "jux_utf8: [$str] = [Réponses 1200+1201+1202]" ) ; - is( '[INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éà çù&*]', jux_utf8( 'INBOX.&AOkA4ADnAPk-&-*' ), "jux_utf8: [INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éà çù&*]" ) ; + is( '[INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éàçù&*]', jux_utf8( 'INBOX.&AOkA4ADnAPk-&-*' ), "jux_utf8: [INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éàçù&*]" ) ; - is( '[&ZTZO9nux-] = [æ”¶ä»¶ç®±]', jux_utf8( '&ZTZO9nux-'), 'jux_utf8: => [&ZTZO9nux-] = [æ”¶ä»¶ç®±]' ) ; + is( '[&ZTZO9nux-] = [收件箱]', jux_utf8( '&ZTZO9nux-'), 'jux_utf8: => [&ZTZO9nux-] = [收件箱]' ) ; # # is( '[!Old Emails]', jux_utf8( '!Old Emails'), 'jux_utf8: !Old Emails => [!Old Emails]' ) ; @@ -9234,7 +10620,7 @@ sub jux_utf8 { #use utf8 ; # juxtapose utf8 at the right if different - my ( $s_utf7 ) = shift ; + my ( $s_utf7 ) = shift @ARG ; my ( $s_utf8 ) = imap_utf7_decode( $s_utf7 ) ; if ( $s_utf7 eq $s_utf8 ) { @@ -9249,14 +10635,14 @@ sub jux_utf8 sub imap_utf7_decode { #use utf8 ; - my ( $s ) = shift ; + my ( $s ) = shift @ARG ; return( Encode::IMAPUTF7::decode("IMAP-UTF-7", $s ) ) ; } sub imap_utf7_encode { #use utf8 ; - my ( $s ) = shift ; + my ( $s ) = shift @ARG ; return( Encode::IMAPUTF7::encode("IMAP-UTF-7", $s ) ) ; } @@ -9264,7 +10650,7 @@ sub imap_utf7_encode sub imap_utf7_encode_old { - my ( $s ) = @_ ; + my ( $s ) = @ARG ; $s = Unicode::String::utf8( $s )->utf7 ; @@ -9279,7 +10665,7 @@ sub imap_utf7_encode_old sub select_folder { - my ( $mysync, $imap, $folder, $hostside ) = @_ ; + my ( $mysync, $imap, $folder, $hostside ) = @ARG ; if ( ! $imap->select( $folder ) ) { my $error = join q{}, "$hostside folder $folder: Could not select: ", @@ -9294,7 +10680,7 @@ sub select_folder sub examine_folder { - my ( $mysync, $imap, $folder, $hostside ) = @_ ; + my ( $mysync, $imap, $folder, $hostside ) = @ARG ; if ( ! $imap->examine( $folder ) ) { my $error = join q{}, "$hostside folder $folder: Could not examine: ", @@ -9335,7 +10721,7 @@ sub create_folder_old myprint( "Folder [$h2_fold] already exists\n" ) ; return( 1 ) ; } - if ( ! $mysync->{dry} ){ + if ( ! $mysync->{ dry } ){ if ( ! $imap->create( $h2_fold ) ) { my $error = join q{}, "Could not create folder [$h2_fold] from [$h1_fold]: ", @@ -9352,13 +10738,13 @@ sub create_folder_old } }else{ # dry mode, no folder so many imap will fail, assuming failure - myprint( "Created ( the old way ) folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; + myprint( "Created ( the old way ) folder [$h2_fold] on host2 $mysync->{ dry_message }\n" ) ; return( 0 ) ; } } -sub create_folder +sub create_folder { my $mysync = shift @ARG ; my( $myimap2 , $h2_fold , $h1_fold ) = @ARG ; @@ -9369,7 +10755,8 @@ sub create_folder return( 0 ) ; } - if ( $create_folder_old ) { + if ( $mysync->{ create_folder_old } ) + { return( create_folder_old( $mysync, $myimap2 , $h2_fold , $h1_fold ) ) ; } @@ -9400,7 +10787,7 @@ sub create_folder create_folder( $mysync, $myimap2 , $parent , $h1_fold ) ; } - if ( ! $mysync->{dry} ) { + if ( ! $mysync->{ dry } ) { if ( ! $myimap2->create( $h2_fold ) ) { my $error = join q{}, "Could not create folder [$h2_fold] from [$h1_fold]: " , @@ -9420,7 +10807,7 @@ sub create_folder } }else{ # dry mode, no folder so many imap will fail, assuming failure - myprint( "Created folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; + myprint( "Created folder [$h2_fold] on host2 $mysync->{ dry_message }\n" ) ; if ( ! $mysync->{ justfolders } ) { myprint( "Since --dry mode is on and folder [$h2_fold] on host2 does not exist yet, syncing messages will not be simulated.\n" . "To simulate message syncing, use --justfolders without --dry to first create the missing folders then rerun the --dry sync.\n" ) ; @@ -9504,7 +10891,7 @@ sub sort_requested_folders sub is_requested_folder { - my ( $folder ) = @_; + my ( $folder ) = @ARG; return( defined $requested_folder{ $folder } ) ; } @@ -9512,7 +10899,7 @@ sub is_requested_folder sub add_to_requested_folders { - my @wanted_folders = @_ ; + my @wanted_folders = @ARG ; foreach my $folder ( @wanted_folders ) { ++$requested_folder{ $folder } ; @@ -9564,7 +10951,7 @@ sub tests_remove_from_requested_folders sub remove_from_requested_folders { - my @unwanted_folders = @_ ; + my @unwanted_folders = @ARG ; my @removed_folders = () ; foreach my $folder ( @unwanted_folders ) { @@ -9579,7 +10966,7 @@ sub remove_from_requested_folders sub compare_lists { - my ($list_1_ref, $list_2_ref) = @_; + my ($list_1_ref, $list_2_ref) = @ARG; return($MINUS_ONE) if ((not defined $list_1_ref) and defined $list_2_ref); return(0) if ((not defined $list_1_ref) and not defined $list_2_ref); # end if no list @@ -9668,7 +11055,7 @@ sub tests_compare_lists sub guess_prefix { - my @foldernames = @_ ; + my @foldernames = @ARG ; my $prefix_guessed = q{} ; foreach my $folder ( @foldernames ) { @@ -9707,7 +11094,7 @@ sub tests_guess_prefix sub get_prefix { - my( $imap, $prefix_in, $prefix_opt, $Side, $folders_ref ) = @_ ; + my( $imap, $prefix_in, $prefix_opt, $Side, $folders_ref ) = @ARG ; my( $prefix_out, $prefix_guessed ) ; ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: Getting prefix\n" ) ; @@ -9743,25 +11130,25 @@ sub get_prefix } -sub guess_separator +sub guess_separator { - my @foldernames = @_ ; + my @foldernames = @ARG ; #return( undef ) unless ( @foldernames ) ; my $sep_guessed ; my %counter ; foreach my $folder ( @foldernames ) { - $counter{'/'}++ while ( $folder =~ m{/}xg ) ; # count / - $counter{'.'}++ while ( $folder =~ m{\.}xg ) ; # count . - $counter{'\\\\'}++ while ( $folder =~ m{(\\){2}}xg ) ; # count \\ - $counter{'\\'}++ while ( $folder =~ m{[^\\](\\){1}(?=[^\\])}xg ) ; # count \ + $counter{ '/' }++ while ( $folder =~ m{/}xg ) ; # count / + $counter{ '.' }++ while ( $folder =~ m{\.}xg ) ; # count . + $counter{ '\\\\' }++ while ( $folder =~ m{(\\){2}}xg ) ; # count \\ + $counter{ '\\' }++ while ( $folder =~ m{[^\\](\\){1}(?=[^\\])}xg ) ; # count \ } my @race_sorted = sort { $counter{ $b } <=> $counter{ $a } } keys %counter ; $sync->{ debug } and myprint( "@foldernames\n@race_sorted\n", %counter, "\n" ) ; $sep_guessed = shift @race_sorted || $LAST_RESSORT_SEPARATOR ; # / when nothing found. return( $sep_guessed ) ; -} +} sub tests_guess_separator { @@ -9782,7 +11169,7 @@ sub tests_guess_separator sub get_separator { - my( $imap, $sep_in, $sep_opt, $Side, $folders_ref ) = @_ ; + my( $imap, $sep_in, $sep_opt, $Side, $folders_ref ) = @ARG ; my( $sep_out, $sep_guessed ) ; ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: Getting separator\n" ) ; @@ -9833,7 +11220,7 @@ sub get_separator sub help_to_guess_sep { - my( $imap, $sep_opt ) = @_ ; + my( $imap, $sep_opt ) = @ARG ; my $help_to_guess_sep = "You can set the separator character with the $sep_opt option,\n" . "the complete listing of folders may help you to find it\n" @@ -9844,7 +11231,7 @@ sub help_to_guess_sep sub help_to_guess_prefix { - my( $imap, $prefix_opt ) = @_ ; + my( $imap, $prefix_opt ) = @ARG ; my $help_to_guess_prefix = "You can set the prefix namespace with the $prefix_opt option,\n" . "the folowing listing of folders may help you to find it:\n" @@ -9856,25 +11243,25 @@ sub help_to_guess_prefix sub folders_list_to_help { - my( $imap ) = shift ; + my( $imap ) = shift @ARG ; my @folders = $imap->folders ; my $listing = join q{}, map { "[$_]\n" } @folders ; return( $listing ) ; } -# Globals are $sync @h1_folders_all @h2_folders_all $prefix1 $prefix2 +# Globals are $sync @h1_folders_all @h2_folders_all sub private_folders_separators_and_prefixes { # what are the private folders separators and prefixes for each server ? - ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Getting separators\n" ) ; - $sync->{ h1_sep } = get_separator( $sync->{imap1}, $sync->{ sep1 }, '--sep1', 'Host1', \@h1_folders_all ) ; - $sync->{ h2_sep } = get_separator( $sync->{imap2}, $sync->{ sep2 }, '--sep2', 'Host2', \@h2_folders_all ) ; + ( $sync->{ debug } or $sync->{ debugfolders } ) and myprint( "Getting separators\n" ) ; + $sync->{ h1_sep } = get_separator( $sync->{ imap1 }, $sync->{ sep1 }, '--sep1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_sep } = get_separator( $sync->{ imap2 }, $sync->{ sep2 }, '--sep2', 'Host2', \@h2_folders_all ) ; - $sync->{ h1_prefix } = get_prefix( $sync->{imap1}, $prefix1, '--prefix1', 'Host1', \@h1_folders_all ) ; - $sync->{ h2_prefix } = get_prefix( $sync->{imap2}, $prefix2, '--prefix2', 'Host2', \@h2_folders_all ) ; + $sync->{ h1_prefix } = get_prefix( $sync->{ imap1 }, $sync->{ prefix1 }, '--prefix1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_prefix } = get_prefix( $sync->{ imap2 }, $sync->{ prefix2 }, '--prefix2', 'Host2', \@h2_folders_all ) ; myprint( "Host1: separator and prefix: [$sync->{ h1_sep }][$sync->{ h1_prefix }]\n" ) ; myprint( "Host2: separator and prefix: [$sync->{ h2_sep }][$sync->{ h2_prefix }]\n" ) ; @@ -9882,9 +11269,9 @@ sub private_folders_separators_and_prefixes } -sub subfolder1 +sub subfolder1 { - my $mysync = shift ; + my $mysync = shift @ARG ; my $subfolder1 = sanitize_subfolder( $mysync->{ subfolder1 } ) ; if ( $subfolder1 ) @@ -9896,7 +11283,7 @@ sub subfolder1 $mysync->{ subfolder1 } = $subfolder1 ; if ( ! add_subfolder1_to_folderrec( $mysync ) ) { - $mysync->{nb_errors}++ ; + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EXIT_SUBFOLDER1_NO_EXISTS, "subfolder1 $subfolder1 does not exist\n" ) ; } } @@ -9906,20 +11293,26 @@ sub subfolder1 } return ; -} +} -sub subfolder2 +sub subfolder2 { - my $mysync = shift ; + my $mysync = shift @ARG ; + + my $subfolder2_option = $mysync->{ subfolder2 } ; my $subfolder2 = sanitize_subfolder( $mysync->{ subfolder2 } ) ; if ( $subfolder2 ) { # turns off automap myprint( "Turning off automapping folders because of --subfolder2\n" ) ; $mysync->{ automap } = undef ; + myprint( "Sanitizing subfolder2: [$mysync->{ subfolder2 }] => [$subfolder2]\n" ) ; $mysync->{ subfolder2 } = $subfolder2 ; + set_regextrans2_for_subfolder2( $mysync ) ; + myprint( "Excluding $subfolder2 because of --subfolder2 $subfolder2_option\n" ) ; + push @exclude, $subfolder2 ; } else { @@ -9927,7 +11320,7 @@ sub subfolder2 } return ; -} +} sub tests_sanitize_subfolder { @@ -9948,7 +11341,7 @@ sub tests_sanitize_subfolder sub sanitize_subfolder { - my $subfolder = shift ; + my $subfolder = shift @ARG ; if ( ! $subfolder ) { @@ -9990,7 +11383,7 @@ sub tests_sanitize_host sub sanitize_host { - my $host = shift ; + my $host = shift @ARG ; if ( ! defined $host ) { return ; } $host =~ tr{ /}{}d ; @@ -9998,82 +11391,299 @@ sub sanitize_host } -sub tests_add_subfolder1_to_folderrec +sub tests_separate_host_port_folder +{ + note( 'Entering tests_separate_host_port_folder()' ) ; + + is_deeply( [], [ separate_host_port_folder( ) ], 'tests_separate_host_port_folder: no args => undef' ) ; + is_deeply( [], [ separate_host_port_folder( '' ) ], 'tests_separate_host_port_folder: empty string => empty array' ) ; + is_deeply( [ 'imap.foo.kk', undef, undef ], [ separate_host_port_folder( 'imap.foo.kk' ) ], 'tests_separate_host_port_folder: imap.foo.kk => imap.foo.kk' ) ; + is_deeply( [ 'imap.foo.kk', undef, '' ], [ separate_host_port_folder( 'imap.foo.kk/' ) ], 'tests_separate_host_port_folder: imap.foo.kk/ => imap.foo.kk' ) ; + is_deeply( [ 'imap.foo.kk', '114433', undef ], [ separate_host_port_folder( 'imap.foo.kk:114433' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433 => imap.foo.kk 114433' ) ; + is_deeply( [ 'imap.foo.kk', '114433', ''], [ separate_host_port_folder( 'imap.foo.kk:114433/' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/ => imap.foo.kk 114433' ) ; + is_deeply( [ 'imap.foo.kk', undef, 'coco' ], [ separate_host_port_folder( 'imap.foo.kk/coco' ) ], 'tests_separate_host_port_folder: imap.foo.kk/coco => imap.foo.kk undef coco' ) ; + is_deeply( [ 'imap.foo.kk', undef, 'coco/cucu/' ], [ separate_host_port_folder( 'imap.foo.kk/coco/cucu/' ) ], 'tests_separate_host_port_folder: imap.foo.kk/coco/cucu/ => imap.foo.kk undef coco/cucu/' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'coco' ], [ separate_host_port_folder( 'imap.foo.kk:114433/coco' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/coco => imap.foo.kk 114433 coco' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'coco/cucu/' ], [ separate_host_port_folder( 'imap.foo.kk:114433/coco/cucu/' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/coco/cucu/ => imap.foo.kk 114433 coco/cucu/' ) ; + + + is_deeply( [ 'imap.foo.kk', undef, 'co co' ], [ separate_host_port_folder( 'imap.foo.kk/co co' ) ], 'tests_separate_host_port_folder: imap.foo.kk/co co => imap.foo.kk undef co co' ) ; + is_deeply( [ 'imap.foo.kk', undef, 'co co/cucu/' ], [ separate_host_port_folder( 'imap.foo.kk/co co/cucu/' ) ], 'tests_separate_host_port_folder: imap.foo.kk/co co/cucu/ => imap.foo.kk undef co co/cucu/' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'co co' ], [ separate_host_port_folder( 'imap.foo.kk:114433/co co' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/co co => imap.foo.kk 114433 co co' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'co co/cucu/' ], [ separate_host_port_folder( 'imap.foo.kk:114433/co co/cucu/' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/co co/cucu/ => imap.foo.kk 114433 co co/cucu/' ) ; + + is_deeply( [ 'imap.foo.kk', undef, 'co co ' ], [ separate_host_port_folder( 'imap.foo.kk/co co ' ) ], 'tests_separate_host_port_folder: imap.foo.kk/co co => imap.foo.kk undef co co ' ) ; + is_deeply( [ 'imap.foo.kk', undef, 'co co/cucu/ ' ], [ separate_host_port_folder( 'imap.foo.kk/co co/cucu/ ' ) ], 'tests_separate_host_port_folder: imap.foo.kk/co co/cucu/ => imap.foo.kk undef co co/cucu/ ' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'co co ' ], [ separate_host_port_folder( 'imap.foo.kk:114433/co co ' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/co co => imap.foo.kk 114433 co co ' ) ; + is_deeply( [ 'imap.foo.kk', '114433', 'co co/cucu/ ' ], [ separate_host_port_folder( 'imap.foo.kk:114433/co co/cucu/ ' ) ], 'tests_separate_host_port_folder: imap.foo.kk:114433/co co/cucu/ => imap.foo.kk 114433 co co/cucu/ ' ) ; + + + note( 'Leaving tests_separate_host_port_folder()' ) ; + return ; +} + + + + + +sub separate_host_port_folder +{ + my $hosturi = shift @ARG ; + + if ( ! $hosturi ) + { + return ; + } + + my ( $host, $port, $folder ) ; + + if ( $hosturi =~ m{\A([a-z0-9_\-.]+)(?:\:([0-9]+))?(?:\/([^\?]*))?\Z}gm ) + { + ( $host, $port, $folder ) = ( $1, $2, $3 ) ; + } + else + { + myprint( "no match host, port, folder for $hosturi\n" ) ; + } + + return( $host, $port, $folder ) ; +} + + + +sub tests_host1_port1_folderrec_from_host1 +{ + note( 'Entering tests_host1_port1_folderrec_from_host1()' ) ; + + is( undef, host1_port1_folderrec_from_host1( ), 'tests_host1_port1_folderrec_from_host1: no args => undef' ) ; + my $mysync = { } ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: { } => undef' ) ; + + $mysync->{ host1 } = '' ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 empty => undef' ) ; + is( '', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 empty => host1 empty' ) ; + + $mysync->{ host1 } = 'imap.foo.kk' ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk => host1 imap.foo.kk' ) ; + + $mysync->{ host1 } = 'imap.foo.kk/' ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk/ => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk/ => host1 imap.foo.kk' ) ; + + $mysync->{ host1 } = 'imap.foo.kk:1234' ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk:1234 => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk:1234 => host1 imap.foo.kk' ) ; + is( '1234', $mysync->{ port1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.kk:1234 => port1 1234' ) ; + + $mysync->{ host1 } = 'imap.foo.qq:4321/' ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.qq:4321/ => undef' ) ; + is( 'imap.foo.qq', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.qq:4321/ => host1 imap.foo.qq' ) ; + is( '4321', $mysync->{ port1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.foo.qq:4321/ => port1 4321' ) ; + + $mysync->{ host1 } = 'imap.bar.kk/prout/hoho' ; + $mysync->{ folderrec } = [ ] ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.kk/prout/hoho => undef' ) ; + is( 'imap.bar.kk', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.kk/prout/hoho => host1 imap.bar.kk' ) ; + is_deeply( [ 'prout/hoho' ], $mysync->{ folderrec }, 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.kk/prout/hoho => folderrec prout/hoho' ) ; + + $mysync->{ host1 } = 'imap.bar.qq:2345/prout.haha' ; + $mysync->{ folderrec } = [ ] ; + is( undef, host1_port1_folderrec_from_host1( $mysync ), 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.qq:2345/prout.haha => undef' ) ; + is( 'imap.bar.qq', $mysync->{ host1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.qq:2345/prout.haha => host1 imap.bar.qq' ) ; + is( '2345', $mysync->{ port1 }, 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.qq:2345/prout.haha => port1 2345' ) ; + is_deeply( [ 'prout.haha' ], $mysync->{ folderrec }, 'tests_host1_port1_folderrec_from_host1: host1 imap.bar.qq:2345/prout.haha => folderrec prout.haha' ) ; + $mysync->{ folderrec } = [ ] ; + + note( 'Leaving tests_host1_port1_folderrec_from_host1()' ) ; + return ; +} + +sub host1_port1_folderrec_from_host1 +{ + my $mysync = shift @ARG ; + + + + my ( $host, $port, $folder ) = separate_host_port_folder( $mysync->{ host1 } ) ; + if ( $host ) + { + $mysync->{ host1 } = $host ; + } + + if ( $port ) + { + myprint( "Acting like --port1 $port\n" ) ; + $mysync->{ port1 } = $port ; + } + + if ( $folder ) + { + myprint( "Acting like --folderrec $folder\n" ) ; + push( @{ $mysync->{ folderrec } }, $folder ) ; + } + + return ; +} + + + +sub tests_host2_port2_subfolder2_from_host2 +{ + note( 'Entering tests_host2_port2_subfolder2_from_host2()' ) ; + + is( undef, host2_port2_subfolder2_from_host2( ), 'tests_host2_port2_subfolder2_from_host2: no args => undef' ) ; + my $mysync = { } ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: { } => undef' ) ; + + $mysync->{ host2 } = '' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 empty => undef' ) ; + is( '', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 empty => host2 empty' ) ; + + $mysync->{ host2 } = 'imap.foo.kk' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk => host2 imap.foo.kk' ) ; + + $mysync->{ host2 } = 'imap.foo.kk/' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk/ => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk/ => host2 imap.foo.kk' ) ; + + $mysync->{ host2 } = 'imap.foo.kk:1234' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk:1234 => undef' ) ; + is( 'imap.foo.kk', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk:1234 => host2 imap.foo.kk' ) ; + is( '1234', $mysync->{ port2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.kk:1234 => port2 1234' ) ; + + $mysync->{ host2 } = 'imap.foo.qq:4321/' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.qq:4321/ => undef' ) ; + is( 'imap.foo.qq', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.qq:4321/ => host2 imap.foo.qq' ) ; + is( '4321', $mysync->{ port2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.foo.qq:4321/ => port2 4321' ) ; + + $mysync->{ host2 } = 'imap.bar.kk/prout/hoho' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.kk/prout/hoho => undef' ) ; + is( 'imap.bar.kk', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.kk/prout/hoho => host2 imap.bar.kk' ) ; + is( 'prout/hoho', $mysync->{ subfolder2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.kk/prout/hoho => subfolder2 prout/hoho' ) ; + + $mysync->{ host2 } = 'imap.bar.qq:2345/prout.haha' ; + is( undef, host2_port2_subfolder2_from_host2( $mysync ), 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.qq:2345/prout.haha => undef' ) ; + is( 'imap.bar.qq', $mysync->{ host2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.qq:2345/prout.haha => host2 imap.bar.qq' ) ; + is( '2345', $mysync->{ port2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.qq:2345/prout.haha => port2 2345' ) ; + is( 'prout.haha', $mysync->{ subfolder2 }, 'tests_host2_port2_subfolder2_from_host2: host2 imap.bar.qq:2345/prout.haha => subfolder2 prout.haha' ) ; + + note( 'Leaving tests_host2_port2_subfolder2_from_host2()' ) ; + return ; +} + +sub host2_port2_subfolder2_from_host2 +{ + my $mysync = shift @ARG ; + + my ( $host, $port, $folder ) = separate_host_port_folder( $mysync->{ host2 } ) ; + if ( $host ) + { + $mysync->{ host2 } = $host ; + } + + if ( $port ) + { + myprint( "Acting like --port2 $port\n" ) ; + $mysync->{ port2 } = $port ; + } + + if ( $folder ) + { + myprint( "Acting like --subfolder2 $folder\n" ) ; + $mysync->{ subfolder2 } = $folder ; + } + + return ; +} + + + + +sub tests_add_subfolder1_to_folderrec { note( 'Entering tests_add_subfolder1_to_folderrec()' ) ; is( undef, add_subfolder1_to_folderrec( ), 'add_subfolder1_to_folderrec: undef => undef' ) ; is_deeply( [], [ add_subfolder1_to_folderrec( ) ], 'add_subfolder1_to_folderrec: no args => empty array' ) ; - @folderrec = () ; my $mysync = {} ; + $mysync->{ folderrec } = [ ] ; is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: empty => empty array' ) ; - is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: empty => empty folderrec' ) ; + is_deeply( [ ], $mysync->{ folderrec }, 'add_subfolder1_to_folderrec: empty => empty folderrec' ) ; $mysync->{ subfolder1 } = 'SUBI' ; $h1_folders_all{ 'SUBI' } = 1 ; $mysync->{ h1_prefix } = 'INBOX/' ; is_deeply( [ 'SUBI' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBI => SUBI' ) ; - is_deeply( [ 'SUBI' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBI => folderrec SUBI ' ) ; + is_deeply( [ 'SUBI' ], $mysync->{ folderrec }, 'add_subfolder1_to_folderrec: SUBI => folderrec SUBI ' ) ; - @folderrec = () ; + $mysync->{ folderrec } = [ ] ; $mysync->{ subfolder1 } = 'SUBO' ; is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO no exists => empty array' ) ; - is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO no exists => empty folderrec' ) ; + is_deeply( [ ], $mysync->{ folderrec }, 'add_subfolder1_to_folderrec: SUBO no exists => empty folderrec' ) ; $h1_folders_all{ 'INBOX/SUBO' } = 1 ; is_deeply( [ 'INBOX/SUBO' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO' ) ; - is_deeply( [ 'INBOX/SUBO' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO folderrec' ) ; - + is_deeply( [ 'INBOX/SUBO' ], $mysync->{ folderrec }, 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO folderrec' ) ; + note( 'Leaving tests_add_subfolder1_to_folderrec()' ) ; return ; -} +} -sub add_subfolder1_to_folderrec +sub add_subfolder1_to_folderrec { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync || ! $mysync->{ subfolder1 } ) { return ; } + if ( ! defined( $mysync->{ folderrec } ) ) { $mysync->{ folderrec } = [] } ; + my $subfolder1 = $mysync->{ subfolder1 } ; my $subfolder1_extended = $mysync->{ h1_prefix } . $subfolder1 ; if ( exists $h1_folders_all{ $subfolder1 } ) { myprint( qq{Acting like --folderrec "$subfolder1"\n} ) ; - push @folderrec, $subfolder1 ; + push @{ $mysync->{ folderrec } }, $subfolder1 ; } elsif ( exists $h1_folders_all{ $subfolder1_extended } ) { myprint( qq{Acting like --folderrec "$subfolder1_extended"\n} ) ; - push @folderrec, $subfolder1_extended ; + push @{ $mysync->{ folderrec } }, $subfolder1_extended ; } else { myprint( qq{Nor folder "$subfolder1" nor "$subfolder1_extended" exists on host1\n} ) ; } - return @folderrec ; -} + return @{ $mysync->{ folderrec } } ; +} -sub set_regextrans2_for_subfolder2 +sub set_regextrans2_for_subfolder2 { - my $mysync = shift ; + my $mysync = shift @ARG ; unshift @{ $mysync->{ regextrans2 } }, + # INBOX/foo/bar => INBOX/subfolder2/foo/bar + # foo/bar => subfolder2/foo/bar q(s,^$mysync->{ h2_prefix }(.*),$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }$1,), + # INBOX => INBOX/subfolder2/INBOX q(s,^INBOX$,$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }INBOX,), + # INBOX/INBOX/ => INBOX/ + # INBOX.INBOX. => INBOX. q(s,^($mysync->{ h2_prefix }){2},$mysync->{ h2_prefix },); - #myprint( "@{ $mysync->{ regextrans2 } }\n" ) ; + $mysync->{ debug } and myprint( "regextrans2: @{ $mysync->{ regextrans2 } }\n" ) ; return ; -} +} # Looks like no globals here -sub tests_imap2_folder_name +sub tests_imap2_folder_name { note( 'Entering tests_imap2_folder_name()' ) ; @@ -10181,8 +11791,8 @@ EOS $mysync->{ h2_sep } = '.'; - set_regextrans2_for_subfolder2( $mysync ) ; $mysync->{ subfolder2 } = 'S1.S2' ; + set_regextrans2_for_subfolder2( $mysync ) ; is( 'S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.F1.F2.F3' ) ; is( 'S1.S2.INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.INBOX' ) ; @@ -10256,10 +11866,28 @@ EOS is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2' ), 'imap2_folder_name: S1/S2 -> INBOX' ) ; is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/' ), 'imap2_folder_name: S1/S2/ -> INBOX' ) ; + # Mike Weaver case. + $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{INBOX.} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + $mysync->{ debug } = 1 ; + + @{ $mysync->{ regextrans2 } } = ( 's,Inbox.,,' ) ; + $mysync->{ f1f2h }{ 'Inbox' } = 'INBOX' ; + is( 'INBOX', imap2_folder_name( $mysync, 'Inbox' ), 'imap2_folder_name: Inbox -> INBOX' ) ; + is( 'INBOX.zozo', imap2_folder_name( $mysync, 'Inbox/zozo' ), 'imap2_folder_name: Inbox/zozo -> INBOX.zozo' ) ; + + $mysync->{ f1f2h }{ 'Inbox' } = 'INBOX.SUB' ; + $mysync->{ subfolder2 } = 'SUB' ; + set_regextrans2_for_subfolder2( $mysync ) ; + is( 'INBOX.SUB', imap2_folder_name( $mysync, 'Inbox' ), 'imap2_folder_name: subfolder2 SUB, Inbox -> INBOX.SUB' ) ; + is( 'INBOX.SUB.zozo', imap2_folder_name( $mysync, 'Inbox/zozo' ), 'imap2_folder_name: subfolder2 SUB, Inbox/zozo -> INBOX.SUB.zozo' ) ; note( 'Leaving tests_imap2_folder_name()' ) ; return ; -} +} # Global variables to remove: @@ -10268,8 +11896,8 @@ EOS sub imap2_folder_name { - my $mysync = shift ; - my ( $h1_fold ) = shift ; + my $mysync = shift @ARG ; + my ( $h1_fold ) = shift @ARG ; my ( $h2_fold ) ; if ( $mysync->{f1f2h}{ $h1_fold } ) { $h2_fold = $mysync->{f1f2h}{ $h1_fold } ; @@ -10322,8 +11950,8 @@ sub tests_remove_last_char_if_is sub remove_last_char_if_is { - my $string = shift ; - my $char = shift ; + my $string = shift @ARG ; + my $char = shift @ARG ; if ( ! defined $string ) { @@ -10400,10 +12028,10 @@ sub tests_prefix_seperator_invertion # Global variables to remove: -sub prefix_seperator_invertion +sub prefix_seperator_invertion { - my $mysync = shift ; - my $h1_fold = shift ; + my $mysync = shift @ARG ; + my $h1_fold = shift @ARG ; my $h2_fold ; if ( not defined $h1_fold ) { return ; } @@ -10415,9 +12043,9 @@ sub prefix_seperator_invertion # first we remove the prefix $h1_fold =~ s/^\Q$my_h1_prefix\E//x ; - ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "removed host1 prefix: [$h1_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{ debugfolders } ) and myprint( "removed host1 prefix: [$h1_fold]\n" ) ; $h2_fold = separator_invert( $mysync, $h1_fold, $my_h1_sep, $my_h2_sep ) ; - ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "inverted separators: [$h2_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{ debugfolders } ) and myprint( "inverted separators: [$h2_fold]\n" ) ; # Adding the prefix supplied by namespace or the --prefix2 option # except for INBOX or Inbox @@ -10426,9 +12054,9 @@ sub prefix_seperator_invertion $h2_fold = $my_h2_prefix . $h2_fold ; } - ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "added host2 prefix: [$h2_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{ debugfolders } ) and myprint( "added host2 prefix: [$h2_fold]\n" ) ; return( $h2_fold ) ; -} +} sub tests_separator_invert { @@ -10460,7 +12088,7 @@ sub tests_separator_invert # sub separator_invert { - my( $mysync, $h1_fold, $h1_separator, $h2_separator ) = @_ ; + my( $mysync, $h1_fold, $h1_separator, $h2_separator ) = @ARG ; return( undef ) if ( not all_defined( $mysync, $h1_fold, $h1_separator, $h2_separator ) ) ; # The separator we hope we'll never encounter: 00000000 == 0x00 @@ -10475,23 +12103,25 @@ sub separator_invert } -sub regextrans2 +sub regextrans2 { - my( $mysync, $h2_fold ) = @_ ; + my( $mysync, $h2_fold ) = @ARG ; # Transforming the folder name by the --regextrans2 option(s) - foreach my $regextrans2 ( @{ $mysync->{ regextrans2 } } ) { + foreach my $regextrans2 ( @{ $mysync->{ regextrans2 } } ) + { my $h2_fold_before = $h2_fold ; my $ret = eval "\$h2_fold =~ $regextrans2 ; 1 " ; ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "[$h2_fold_before] -> [$h2_fold] using regextrans2 [$regextrans2]\n" ) ; - if ( not ( defined $ret ) or $EVAL_ERROR ) { - $mysync->{nb_errors}++ ; + if ( not ( defined $ret ) or $EVAL_ERROR ) + { + $mysync->{ nb_errors }++ ; exit_clean( $mysync, $EX_USAGE, "error: eval regextrans2 '$regextrans2': $EVAL_ERROR\n" ) ; } } return( $h2_fold ) ; -} +} sub tests_decompose_regex @@ -10508,7 +12138,7 @@ sub tests_decompose_regex sub decompose_regex { - my $regex = shift ; + my $regex = shift @ARG ; my( $left_part, $right_part ) ; ( $left_part, $right_part ) = $regex =~ m{^s/((?:[^/]|\\/)+)/((?:[^/]|\\/)+)/}x; @@ -10518,26 +12148,28 @@ sub decompose_regex -sub tests_timenext +sub tests_timenext { note( 'Entering tests_timenext()' ) ; - is( undef, timenext( ), 'timenext: no args => undef' ) ; + is( undef, timenext( ), 'tests_timenext: no args => undef' ) ; my $mysync ; - is( undef, timenext( $mysync ), 'timenext: undef => undef' ) ; + is( undef, timenext( $mysync ), 'tests_timenext: undef => undef' ) ; $mysync = {} ; - ok( time - timenext( $mysync ) <= 1e-02, 'timenext: defined first time => ~ time' ) ; - ok( timenext( $mysync ) <= 1e-02, 'timenext: second time => less than 1e-02' ) ; - ok( timenext( $mysync ) <= 1e-02, 'timenext: third time => less than 1e-02' ) ; + ok( time - timenext( $mysync ) <= 2e-02, 'tests_timenext: defined first time => ~ time' ) ; + ok( timenext( $mysync ) <= 2e-02, 'tests_timenext: second time => less than 2e-02' ) ; + ok( timenext( $mysync ) <= 2e-02, 'tests_timenext: third time => less than 2e-02' ) ; note( 'Leaving tests_timenext()' ) ; return ; -} +} + + sub timenext { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! defined $mysync ) { @@ -10558,9 +12190,9 @@ sub tests_timesince { note( 'Entering tests_timesince()' ) ; - ok( timesince( time - 1 ) - 1 <= 1e-02, 'timesince: time - 1 => <= 1 + 1e-02' ) ; - ok( timesince( time ) <= 1e-02, 'timesince: time => <= 1e-02' ) ; - ok( timesince( ) - time <= 1e-02, 'timesince: no args => <= time + 1e-02' ) ; + ok( timesince( time - 1 ) - 1 <= 1e-02, 'tests_timesince: time - 1 => <= 1 + 1e-02' ) ; + ok( timesince( time ) <= 1e-02, 'tests_timesince: time => <= 1e-02' ) ; + ok( timesince( ) - time <= 1e-02, 'tests_timesince: no args => <= time + 1e-02' ) ; note( 'Leaving tests_timesince()' ) ; return ; } @@ -10573,7 +12205,7 @@ sub timesince my ( $timenow, $timediff ) ; $timenow = time ; $timediff = $timenow - $timeinit ; - # Often used in a division so no 0 but a nano seconde. + # Often used in a division so no 0 but a nano second. return( max( $timediff, min( 1e-09, $timediff ) ) ) ; } @@ -10584,57 +12216,57 @@ sub tests_regexflags { note( 'Entering tests_regexflags()' ) ; - my $mysync = {} ; - - ok( q{} eq regexflags( $mysync, q{} ), 'regexflags, null string q{}' ) ; - ok( q{\Seen NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, nothing to do} ) ; + my $mysync = {} ; + + ok( q{} eq regexflags( $mysync, q{} ), 'tests_regexflags, null string q{}' ) ; + ok( q{\Seen NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{tests_regexflags, nothing to do} ) ; @{ $mysync->{ regexflag } } = ('I am BAD' ) ; - ok( not ( defined regexflags( $mysync, q{} ) ), 'regexflags, bad regex' ) ; + ok( not ( defined regexflags( $mysync, q{} ) ), 'tests_regexflags, bad regex' ) ; @{ $mysync->{ regexflag } } = ( 's/NonJunk//g' ) ; - ok( q{\Seen $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove NonJunk: 's/NonJunk//g'} ) ; + ok( q{\Seen $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{tests_regexflags, remove NonJunk: 's/NonJunk//g'} ) ; @{ $mysync->{ regexflag } } = ( q{s/\$Spam//g} ) ; - ok( q{\Seen NonJunk } eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove $Spam: 's/\$Spam//g'} ) ; + ok( q{\Seen NonJunk } eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{tests_regexflags, remove $Spam: 's/\$Spam//g'} ) ; @{ $mysync->{ regexflag } } = ( 's/\\\\Seen//g' ) ; - ok( q{ NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove \Seen: 's/\\\\\\\\Seen//g'} ) ; + ok( q{ NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{tests_regexflags, remove \Seen: 's/\\\\\\\\Seen//g'} ) ; @{ $mysync->{ regexflag } } = ( 's/(\s|^)[^\\\\]\w+//g' ) ; - ok( q{\Seen \Middle \End} eq regexflags( $mysync, q{\Seen NonJunk \Middle $Spam \End} ), q{regexflags: only \word among \Seen NonJunk \Middle $Spam \End} ) ; + ok( q{\Seen \Middle \End} eq regexflags( $mysync, q{\Seen NonJunk \Middle $Spam \End} ), q{tests_regexflags: only \word among \Seen NonJunk \Middle $Spam \End} ) ; ok( q{ \Seen \Middle \End1} eq regexflags( $mysync, q{Begin \Seen NonJunk \Middle $Spam \End1 End} ), - q{regexflags: only \word among Begin \Seen NonJunk \Middle $Spam \End1 End} ) ; + q{tests_regexflags: only \word among Begin \Seen NonJunk \Middle $Spam \End1 End} ) ; @{ $mysync->{ regexflag } } = ( q{s/.*?(Keep1|Keep2|Keep3)/$1 /g} ) ; - ok( 'Keep1 Keep2 ReB' eq regexflags( $mysync, 'ReA Keep1 REM Keep2 ReB' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ReB' eq regexflags( $mysync, 'ReA Keep1 REM Keep2 ReB' ), 'tests_regexflags, Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM REM Keep1 Keep2' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM REM Keep2' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM REM Keep2' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2' ), 'Keep only regex' ) ; - ok( 'Keep1 ' eq regexflags( $mysync, 'REM Keep1' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM REM Keep1 Keep2' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM REM Keep2' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM REM Keep2' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 ' eq regexflags( $mysync, 'REM Keep1' ), 'tests_regexflags,Keep only regex' ) ; @{ $mysync->{ regexflag } } = ( q{s/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g} ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 ReB' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 REM REM REM' ), 'Keep only regex' ) ; - ok( 'Keep2 ' eq regexflags( $mysync, 'Keep2 REM REM REM' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 ReB' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 REM REM REM' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep2 ' eq regexflags( $mysync, 'Keep2 REM REM REM' ), 'tests_regexflags,Keep only regex' ) ; @{ $mysync->{ regexflag } } = ( q{s/.*?(Keep1|Keep2|Keep3)/$1 /g}, 's/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM Keep2 REM' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 Keep2 REM' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep2 Keep3 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM REM Keep3 REM' ), 'Keep only regex' ) ; - ok( 'Keep1 ' eq regexflags( $mysync, 'REM REM Keep1 REM REM REM ' ), 'Keep only regex' ) ; - ok( 'Keep1 Keep3 ' eq regexflags( $mysync, 'RE1 Keep1 RE2 Keep3 RE3 RE4 RE5 ' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM Keep2 REM' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 Keep2 REM' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep2 Keep3 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM REM Keep3 REM' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 ' eq regexflags( $mysync, 'REM REM Keep1 REM REM REM ' ), 'tests_regexflags,Keep only regex' ) ; + ok( 'Keep1 Keep3 ' eq regexflags( $mysync, 'RE1 Keep1 RE2 Keep3 RE3 RE4 RE5 ' ), 'tests_regexflags,Keep only regex' ) ; @{ $mysync->{ regexflag } } = ( 's/(.*)/$1 jrdH8u/' ) ; - ok('REM REM REM REM REM jrdH8u' eq regexflags( $mysync, 'REM REM REM REM REM' ), q{Add jrdH8u 's/(.*)/\$1 jrdH8u/'} ) ; + ok('REM REM REM REM REM jrdH8u' eq regexflags( $mysync, 'REM REM REM REM REM' ), q{tests_regexflags, Add jrdH8u 's/(.*)/\$1 jrdH8u/'} ) ; @{ $mysync->{ regexflag } } = ('s/jrdH8u *//' ); - ok('REM REM REM REM REM ' eq regexflags( $mysync, 'REM REM REM REM REM jrdH8u' ), q{Remove jrdH8u s/jrdH8u *//} ) ; + ok('REM REM REM REM REM ' eq regexflags( $mysync, 'REM REM REM REM REM jrdH8u' ), q{tests_regexflags, Remove jrdH8u s/jrdH8u *//} ) ; @{ $mysync->{ regexflag } } = ( 's/.*?(?:(\\\\(?:Answered|Flagged|Deleted|Seen|Draft)\s?)|$)/defined($1)?$1:q()/eg' @@ -10642,27 +12274,27 @@ sub tests_regexflags ok( '\\Deleted \\Answered ' eq regexflags( $mysync, 'Blabla \$Junk \\Deleted machin \\Answered truc' ), - 'Keep only regex: Exchange case (Phil)' ) ; + 'tests_regexflags, Keep only regex: Exchange case (Phil)' ) ; - ok( q{} eq regexflags( $mysync, q{} ), 'Keep only regex: Exchange case, null string (Phil)' ) ; + ok( q{} eq regexflags( $mysync, q{} ), 'tests_regexflags, Keep only regex: Exchange case, null string (Phil)' ) ; ok( q{} eq regexflags( $mysync, 'Blabla $Junk machin truc' ), - 'Keep only regex: Exchange case, no accepted flags (Phil)' ) ; + 'tests_regexflags, Keep only regex: Exchange case, no accepted flags (Phil)' ) ; ok('\\Deleted \\Answered \\Draft \\Flagged ' eq regexflags( $mysync, '\\Deleted \\Answered \\Draft \\Flagged ' ), - 'Keep only regex: Exchange case (Phil)' ) ; + 'tests_regexflags, Keep only regex: Exchange case (Phil)' ) ; @{ $mysync->{ regexflag } } = ( 's/\\\\Flagged//g' ) ; is('\Deleted \Answered \Draft ', regexflags( $mysync, '\\Deleted \\Answered \\Draft \\Flagged ' ), - 'regexflags: remove \Flagged 1' ) ; + 'tests_regexflags, remove \Flagged 1' ) ; is('\\Deleted \\Answered \\Draft', regexflags( $mysync, '\\Deleted \\Flagged \\Answered \\Draft' ), - 'regexflags: remove \Flagged 2' ) ; + 'tests_regexflags, remove \Flagged 2' ) ; # I didn't understand why it gives \F # https://perldoc.perl.org/perlrebackslash.html @@ -10676,48 +12308,72 @@ sub tests_regexflags #regexflags( '\Deleted Flagged \Answered \Flagged \Draft \Flagged' ), # 'regexflags: remove \Flagged 3 mistery...' ) ; - $mysync->{ regexflag } = [ ] ; - $mysync->{ filterbuggyflags } = 1 ; - filterbuggyflags( $mysync ) ; - - is( '\Deleted \Answered \Draft \Flagged', - regexflags( $mysync, '\\Deleted \\Answered \\RECEIPTCHECKED \\Draft \\Indexed \\Flagged' ), - 'regexflags: remove famous /X 1' ) ; - - is( '\\Deleted \\Flagged \\Answered \\Draft', - regexflags( $mysync, '\\Deleted \\RECEIPTCHECKED \\Flagged \\Answered \\Indexed \\Draft' ), - 'regexflags: remove famous /X 2' ) ; - - is( '\ ', '\\ ', 'regexflags: \ is \\ ' ) ; - is( '\\ ', '\\ ', 'regexflags: \\ is \\ ' ) ; - is( '\\ \ ', '\ \\ ', 'regexflags: \\ \ is \ \\ ' ) ; + is( '\\ \ ', '\ \\ ', 'tests_regexflags, \\ \ is \ \\ ' ) ; note( 'Leaving tests_regexflags()' ) ; return ; } -sub regexflags +sub regexflags { - my $mysync = shift ; - my $flags = shift ; - + my $mysync = shift @ARG ; + my $flags = shift @ARG ; + foreach my $regexflag ( @{ $mysync->{ regexflag } } ) { my $flags_orig = $flags ; - $debugflags and myprint( "eval \$flags =~ $regexflag\n" ) ; + $mysync->{ debugflags } and myprint( "eval \$flags =~ $regexflag\n" ) ; my $ret = eval "\$flags =~ $regexflag ; 1 " ; - $debugflags and myprint( "regexflag $regexflag [$flags_orig] -> [$flags]\n" ) ; + $mysync->{ debugflags } and myprint( "regexflag $regexflag [$flags_orig] -> [$flags]\n" ) ; if( not ( defined $ret ) or $EVAL_ERROR ) { myprint( "Error: eval regexflag '$regexflag': $EVAL_ERROR\n" ) ; return( undef ) ; } } return( $flags ) ; +} + +sub tests_filterbuggyflags +{ + note( 'Entering tests_filterbuggyflags()' ) ; + + my $mysync = {} ; + + + $mysync->{ regexflag } = [ ] ; + $mysync->{ filterbuggyflags } = 1 ; + filterbuggyflags( $mysync ) ; + + # + is( '\Deleted \Answered \Draft \Flagged ', + regexflags( $mysync, '\\Deleted \\Answered \\RECEIPTCHECKED \\Draft \\Indexed \\Flagged \\JUNK \\Junk' ), + 'tests_filterbuggyflags: remove famous /X 1' ) ; + + is( '\\Deleted \\Flagged \\Answered \\Draft', + regexflags( $mysync, '\\Deleted \\RECEIPTCHECKED \\Flagged \\Answered \\Indexed \\Draft' ), + 'tests_filterbuggyflags: remove famous /X 2' ) ; + + is( '\ ', '\\ ', 'tests_filterbuggyflags: \ is \\ ' ) ; + is( '\\ ', '\\ ', 'tests_filterbuggyflags: \\ is \\ ' ) ; + + + note( 'Leaving tests_filterbuggyflags()' ) ; + return ; + + +} + +sub buggyflagsregex +{ + # From /X analyse + # cut -d: -f1 Error_112_all_syncs.txt | xargs egrep -oih 'Invalid system flag [^( ]+' | sort | uniq -c | sort -g + my @buggyflagsregex = ( 's/\\\\RECEIPTCHECKED|\\\\JUNK|\\\\Indexed|\\\\X-EON-HAS-ATTACHMENT|\\\\UNSEEN|\\\\ATTACHED|\\\\X-HAS-ATTACH|\\\\FORWARDED|\\\\FORWARD|\\\\X-FORWARDED|\\\\\$FORWARDED|\\\\PRIORITY|\\\\READRCPT//gi' ) ; + return( @buggyflagsregex ) ; } sub filterbuggyflags { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( $mysync->{ filterbuggyflags } ) { unshift @{ $mysync->{ regexflag } }, buggyflagsregex( ) ; @@ -10730,24 +12386,24 @@ sub tests_remove_doublequotes_if_any { note( 'Entering tests_remove_doublequotes_if_any()' ) ; # the number of tests is stupid here - is( undef, remove_doublequotes_if_any( ), 'remove_doublequotes_if_any: no args => undef' ) ; - is( q{}, remove_doublequotes_if_any( q{} ), 'remove_doublequotes_if_any: empty string => empty string' ) ; - is( q{}, remove_doublequotes_if_any( q{""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; - is( q{}, remove_doublequotes_if_any( q{"""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; - is( q{}, remove_doublequotes_if_any( q{"""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; - is( q{toto}, remove_doublequotes_if_any( q{"toto"} ), 'remove_doublequotes_if_any: "toto" => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{toto} ), 'remove_doublequotes_if_any: toto => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{to"to} ), 'remove_doublequotes_if_any: to"to => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{toto"} ), 'remove_doublequotes_if_any: toto" => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{"toto} ), 'remove_doublequotes_if_any: "toto => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{"to"to} ), 'remove_doublequotes_if_any: "to"to => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{to"to"} ), 'remove_doublequotes_if_any: to"to" => toto' ) ; + is( undef, remove_doublequotes_if_any( ), 'tests_remove_doublequotes_if_any: no args => undef' ) ; + is( q{}, remove_doublequotes_if_any( q{} ), 'tests_remove_doublequotes_if_any: empty string => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{""} ), 'tests_remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{"""} ), 'tests_remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{"""} ), 'tests_remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"toto"} ), 'tests_remove_doublequotes_if_any: "toto" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto} ), 'tests_remove_doublequotes_if_any: toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to"to} ), 'tests_remove_doublequotes_if_any: to"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto"} ), 'tests_remove_doublequotes_if_any: toto" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"toto} ), 'tests_remove_doublequotes_if_any: "toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"to"to} ), 'tests_remove_doublequotes_if_any: "to"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to"to"} ), 'tests_remove_doublequotes_if_any: to"to" => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{to\"to} ), 'remove_doublequotes_if_any: to\"to => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{toto\"} ), 'remove_doublequotes_if_any: toto\" => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{\"toto} ), 'remove_doublequotes_if_any: \"toto => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{\"to\"to} ), 'remove_doublequotes_if_any: \"to\"to => toto' ) ; - is( q{toto}, remove_doublequotes_if_any( q{to\"to\"} ), 'remove_doublequotes_if_any: to\"to" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to\"to} ), 'tests_remove_doublequotes_if_any: to\"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto\"} ), 'tests_remove_doublequotes_if_any: toto\" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{\"toto} ), 'tests_remove_doublequotes_if_any: \"toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{\"to\"to} ), 'tests_remove_doublequotes_if_any: \"to\"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to\"to\"} ), 'tests_remove_doublequotes_if_any: to\"to" => toto' ) ; note( 'Leaving tests_remove_doublequotes_if_any()' ) ; @@ -10758,7 +12414,7 @@ sub tests_remove_doublequotes_if_any sub remove_doublequotes_if_any { - my $string = shift ; + my $string = shift @ARG ; if ( ! defined $string ) { return ; } $string =~ s/\\\"//g ; @@ -10774,11 +12430,11 @@ sub acls_sync # Standard Rights: # https://tools.ietf.org/html/rfc4314#section-2.1 - my( $mysync, $h1_fold, $h2_fold ) = @_ ; + my( $mysync, $h1_fold, $h2_fold ) = @ARG ; if ( $mysync->{ syncacls } ) { - my $h1_hash = $mysync->{imap1}->getacl($h1_fold) + my $h1_hash = $mysync->{ imap1 }->getacl($h1_fold) or myprint( "Host1: Could not getacl for $h1_fold: $EVAL_ERROR\n" ) ; - my $h2_hash = $mysync->{imap2}->getacl($h2_fold) + my $h2_hash = $mysync->{ imap2 }->getacl($h2_fold) or myprint( "Host2: Could not getacl for $h2_fold: $EVAL_ERROR\n" ) ; my %users = map { ($_, 1) } ( keys %{ $h1_hash} , keys %{ $h2_hash } ) ; @@ -10797,9 +12453,9 @@ sub acls_sync myprint( "Host2: user $user_no_quotes has already the same acl, no need to set it.\n" ) ; next ; } - myprint( "Host2: setting acl for folder $h2_fold user $user_no_quotes acl $h1_acl $mysync->{dry_message}\n" ) ; - unless ( $mysync->{dry} ) { - $mysync->{imap2}->setacl( $h2_fold, $user_no_quotes, $h1_acl ) + myprint( "Host2: setting acl for folder $h2_fold user $user_no_quotes acl $h1_acl $mysync->{ dry_message }\n" ) ; + unless ( $mysync->{ dry } ) { + $mysync->{ imap2 }->setacl( $h2_fold, $user_no_quotes, $h1_acl ) or myprint( "Could not set acl for user $user_no_quotes on host2: $EVAL_ERROR\n" ) ; } } @@ -10808,39 +12464,39 @@ sub acls_sync } -sub tests_permanentflags +sub tests_permanentflags { note( 'Entering tests_permanentflags()' ) ; my $mysync = { } ; ok( q{} eq permanentflags( $mysync, ' * OK [PERMANENTFLAGS (\* \Draft \Answered)] Limited' ), 'permanentflags \*' ) ; - + ok( '\Draft \Answered' eq permanentflags( $mysync, ' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited' ), 'permanentflags \Draft \Answered' ) ; - + ok( '\Draft \Answered' eq permanentflags( $mysync, 'Blabla', ' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited', 'Blabla' ), 'permanentflags \Draft \Answered' ) ; - + ok( q{} eq permanentflags( $mysync, 'Blabla' ), 'permanentflags nothing' ) ; note( 'Leaving tests_permanentflags()' ) ; return ; } -sub permanentflags +sub permanentflags { - my $mysync = shift ; + my $mysync = shift @ARG ; + + my @lines = @ARG ; - my @lines = @_ ; - - foreach my $line (@lines) { + foreach my $line ( @lines ) { if ( $line =~ m{\[PERMANENTFLAGS\s\(([^)]+?)\)\]}x ) { - ( $debugflags or $sync->{ debug } ) and myprint( "permanentflags: $line" ) ; + ( $mysync->{ debugflags } or $mysync->{ debug } ) and myprint( "permanentflags: $line" ) ; my $permanentflags = $1 ; if ( $permanentflags =~ m{\\\*}x ) { @@ -10850,7 +12506,7 @@ sub permanentflags } ; } return( q{} ) ; -} +} sub tests_flags_filter { @@ -10871,7 +12527,7 @@ sub tests_flags_filter sub flags_filter { - my( $flags, $allowed_flags ) = @_ ; + my( $flags, $allowed_flags ) = @ARG ; my @flags = split /\s+/x, $flags ; my %allowed_flags = map { $_ => 1 } split q{ }, $allowed_flags ; @@ -10901,7 +12557,7 @@ sub tests_flagscase sub flagscase { - my $flags = shift ; + my $flags = shift @ARG ; my @flags = split /\s+/x, $flags ; my %rfc_flags = map { $_ => 1 } split q{ }, '\Answered \Flagged \Deleted \Seen \Draft' ; @@ -10919,37 +12575,37 @@ sub tests_flags_for_host2 note( 'Entering tests_flags_for_host2()' ) ; is( undef, flags_for_host2( ), 'flags_for_host2: no args => undef' ) ; - + my $mysync ; is( undef, flags_for_host2( $mysync ), 'flags_for_host2: undef => undef' ) ; - + $mysync = { } ; is( undef, flags_for_host2( $mysync ), 'flags_for_host2: nothing => undef' ) ; - + is( q{}, flags_for_host2( $mysync, '' ), 'flags_for_host2: no flags => empty string' ) ; - + is( q{}, flags_for_host2( $mysync, '\Recent' ), 'flags_for_host2: \Recent => empty string' ) ; - + is( q{\Seen}, flags_for_host2( $mysync, '\Recent \Seen' ), 'flags_for_host2: \Recent \Seen => \Seen' ) ; - + is( q{\Deleted \Seen}, flags_for_host2( $mysync, '\Deleted \Recent \Seen' ), 'flags_for_host2: \Deleted \Recent \Seen => \Deleted \Seen' ) ; - + $mysync->{ flagscase } = 0 ; is( q{\DELETED \Seen}, flags_for_host2( $mysync, '\DELETED \Seen' ), 'flags_for_host2: flagscase = 0 \DELETED \Seen => \DELETED \Seen' ) ; - + $mysync->{ flagscase } = 1 ; is( q{\Deleted \Seen}, flags_for_host2( $mysync, '\DELETED \Seen' ), 'flags_for_host2: flagscase = 1 \DELETED \Seen => \Deleted \Seen' ) ; - + $mysync->{ filterflags } = 0 ; is( q{\Seen \Blabla}, flags_for_host2( $mysync, '\Seen \Blabla', '\Seen \Junk' ), 'flags_for_host2: filterflags = 0 \Seen \Blabla among \Seen \Junk => \Seen \Blabla' ) ; - + $mysync->{ filterflags } = 1 ; is( q{\Seen}, flags_for_host2( $mysync, '\Seen \Blabla', '\Seen \Junk' ), 'flags_for_host2: filterflags = 1 \Seen \Blabla among \Seen \Junk => \Seen' ) ; - + $mysync->{ filterflags } = 1 ; is( q{\Seen \Blabla}, flags_for_host2( $mysync, '\Seen \Blabla', '' ), 'flags_for_host2: filterflags = 1 \Seen \Blabla among "" => \Seen \Blabla' ) ; - - + + note( 'Leaving tests_flags_for_host2()' ) ; return ; } @@ -10959,30 +12615,30 @@ sub tests_flags_for_host2 sub flags_for_host2 { - my $mysync = shift ; - my $h1_flags = shift ; - my $permanentflags2 = shift ; - - if ( ! all_defined( $mysync, $h1_flags ) ) { return ; } ; - + my $mysync = shift @ARG ; + my $h1_flags = shift @ARG ; + my $permanentflags2 = shift @ARG ; + + if ( ! all_defined( $mysync, $h1_flags ) ) { return ; } ; + # RFC 2060: This flag can not be altered by any client $h1_flags =~ s@\\Recent\s?@@xgi ; - + my $h1_flags_re ; if ( $mysync->{ regexflag } and defined( $h1_flags_re = regexflags( $mysync, $h1_flags ) ) ) { $h1_flags = $h1_flags_re ; } - + if ( $mysync->{ flagscase } ) { $h1_flags = flagscase( $h1_flags ) ; } - + if ( $permanentflags2 and $mysync->{ filterflags } ) { $h1_flags = flags_filter( $h1_flags, $permanentflags2 ) ; } - + return( $h1_flags ) ; } @@ -10990,7 +12646,7 @@ sub flags_for_host2 sub ucsecond { - my $string = shift ; + my $string = shift @ARG ; my $output ; return( $string ) if ( 1 >= length $string ) ; @@ -11021,21 +12677,21 @@ sub tests_ucsecond sub select_msgs { - my ( $imap, $msgs_all_hash_ref, $search_cmd, $abletosearch, $folder ) = @_ ; + my ( $mysync, $imap, $msgs_all_hash_ref, $search_cmd, $abletosearch, $folder ) = @ARG ; my ( @msgs ) ; if ( $abletosearch ) { - @msgs = select_msgs_by_search( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; + @msgs = select_msgs_by_search( $mysync, $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; }else{ - @msgs = select_msgs_by_fetch( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; + @msgs = select_msgs_by_fetch( $mysync, $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; } return( @msgs ) ; } -sub select_msgs_by_search -{ - my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; +sub select_msgs_by_search +{ + my ( $mysync, $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @ARG ; my ( @msgs, @msgs_all ) ; # Need to have the whole list in msgs_all_hash_ref @@ -11043,11 +12699,10 @@ sub select_msgs_by_search # Need all messages list to avoid deleting useful cache part # in case of --search or --minage or --maxage - if ( ( defined $msgs_all_hash_ref and $usecache ) - or ( not defined $maxage and not defined $minage and not defined $search_cmd ) - ) { - - $debugdev and myprint( "Calling messages()\n" ) ; + if ( ( defined $msgs_all_hash_ref and $mysync->{ usecache } ) + or ( not defined $mysync->{ maxage } and not defined $mysync->{ minage } and not defined $search_cmd ) + ) + { @msgs_all = $imap->messages( ) ; return if ( $#msgs_all == 0 && !defined $msgs_all[0] ) ; @@ -11056,25 +12711,48 @@ sub select_msgs_by_search @{ $msgs_all_hash_ref }{ @msgs_all } = () ; } # return all messages - if ( not defined $maxage and not defined $minage and not defined $search_cmd ) { + if ( not defined $mysync->{ maxage } and not defined $mysync->{ minage } and not defined $search_cmd ) { return( @msgs_all ) ; } } - if ( defined $search_cmd ) { - @msgs = $imap->search( $search_cmd ) ; + if ( defined( $search_cmd ) ) + { + #@msgs = $imap->search( $search_cmd ) ; + @msgs = select_msgs_by_search_cmd( $mysync, $imap, $search_cmd ) ; return( @msgs ) ; } - # we are here only if $maxage or $minage is defined - @msgs = select_msgs_by_age( $imap ) ; + # we are here only if $mysync->{ maxage } or $mysync->{ minage } is defined + @msgs = select_msgs_by_age( $mysync, $imap ) ; return( @msgs ); +} + + + + + +sub select_msgs_by_search_cmd +{ + my ( $mysync, $imap, $search_cmd ) = @ARG ; + my ( @msgs ) ; + + @msgs = $imap->search( $search_cmd ) ; + if ( $@ ) + { + # Error + errors_incr( $mysync, "$@\n" ) ; + return ; + } + else + { + return( @msgs ) ; + } } - sub select_msgs_by_fetch -{ - my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; +{ + my ( $mysync, $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @ARG ; my ( @msgs, @msgs_all, %fetch ) ; # Need to have the whole list in msgs_all_hash_ref @@ -11082,13 +12760,10 @@ sub select_msgs_by_fetch # Need all messages list to avoid deleting useful cache part # in case of --search or --minage or --maxage - - $debugdev and myprint( "Calling fetch_hash()\n" ) ; - my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + my $fetch_hash_uids = $mysync->{ fetch_hash_set } || "1:*" ; %fetch = %{$imap->fetch_hash( $fetch_hash_uids, 'INTERNALDATE' ) } ; @msgs_all = sort { $a <=> $b } keys %fetch ; - $debugdev and myprint( "Done fetch_hash()\n" ) ; return if ( $#msgs_all == 0 && !defined $msgs_all[0] ) ; @@ -11096,78 +12771,79 @@ sub select_msgs_by_fetch @{ $msgs_all_hash_ref }{ @msgs_all } = () ; } # return all messages - if ( not defined $maxage and not defined $minage and not defined $search_cmd ) { + if ( not defined $mysync->{ maxage } and not defined $mysync->{ minage } and not defined $search_cmd ) { return( @msgs_all ) ; } if ( defined $search_cmd ) { myprint( "Warning: strange to see --search with --noabletosearch, an error can happen\n" ) ; - @msgs = $imap->search( $search_cmd ) ; + @msgs = select_msgs_by_search_cmd( $mysync, $imap, $search_cmd ) ; return( @msgs ) ; } # we are here only if $maxage or $minage is defined my( @max, @min, $maxage_epoch, $minage_epoch ) ; - if ( defined $maxage ) { $maxage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ; } - if ( defined $minage ) { $minage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ; } + if ( defined $mysync->{ maxage } ) { $maxage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $mysync->{ maxage } ; } + if ( defined $mysync->{ minage } ) { $minage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $mysync->{ minage } ; } foreach my $msg ( @msgs_all ) { my $idate = $fetch{ $msg }->{'INTERNALDATE'} ; #myprint( "$idate\n" ) ; - if ( defined $maxage and ( epoch( $idate ) >= $maxage_epoch ) ) { + if ( defined $mysync->{ maxage } and ( epoch( $idate ) >= $maxage_epoch ) ) { push @max, $msg ; } - if ( defined $minage and ( epoch( $idate ) <= $minage_epoch ) ) { - push @min, $msg ; + if ( defined $mysync->{ minage } and ( epoch( $idate ) <= $minage_epoch ) ) { + push @min, $msg ; } } - @msgs = msgs_from_maxmin( \@max, \@min ) ; + @msgs = msgs_from_maxmin( $mysync, \@max, \@min ) ; return( @msgs ) ; } -sub select_msgs_by_age +sub select_msgs_by_age { - my( $imap ) = @_ ; + my( $mysync, $imap ) = @ARG ; my( @max, @min, @msgs, @inter, @union ) ; - if ( defined $maxage ) { - @max = $imap->sentsince( $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ) ; + if ( defined $mysync->{ maxage } ) + { + @max = $imap->sentsince( $timestart_int - $NB_SECONDS_IN_A_DAY * $mysync->{ maxage } ) ; } - if ( defined $minage ) { - @min = $imap->sentbefore( $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ) ; + if ( defined $mysync->{ minage } ) + { + @min = $imap->sentbefore( $timestart_int - $NB_SECONDS_IN_A_DAY * $mysync->{ minage } ) ; } - @msgs = msgs_from_maxmin( \@max, \@min ) ; + @msgs = msgs_from_maxmin( $mysync, \@max, \@min ) ; return( @msgs ) ; -} +} -sub msgs_from_maxmin +sub msgs_from_maxmin { - my( $max_ref, $min_ref ) = @_ ; + my( $mysync, $max_ref, $min_ref ) = @ARG ; my( @max, @min, @msgs, @inter, @union ) ; @max = @{ $max_ref } ; @min = @{ $min_ref } ; SWITCH: { - if ( not ( defined $minage or defined $maxage ) ) + if ( not ( defined $mysync->{ minage } or defined $mysync->{ maxage } ) ) { return ; } - unless( defined $minage ) { @msgs = @max ; last SWITCH } ; - unless( defined $maxage ) { @msgs = @min ; last SWITCH } ; + unless( defined $mysync->{ minage } ) { @msgs = @max ; last SWITCH } ; + unless( defined $mysync->{ maxage } ) { @msgs = @min ; last SWITCH } ; my ( %union, %inter ) ; foreach my $m ( @min, @max ) { $union{ $m }++ && $inter{ $m }++ } @inter = sort { $a <=> $b } keys %inter ; @union = sort { $a <=> $b } keys %union ; # normal case - if ( $minage <= $maxage ) { @msgs = @inter ; last SWITCH } ; + if ( $mysync->{ minage } <= $mysync->{ maxage } ) { @msgs = @inter ; last SWITCH } ; # just exclude messages between - if ( $minage > $maxage ) { @msgs = @union ; last SWITCH } ; - + if ( $mysync->{ minage } > $mysync->{ maxage } ) { @msgs = @union ; last SWITCH } ; } return( @msgs ) ; -} +} sub tests_msgs_from_maxmin { @@ -11176,32 +12852,32 @@ sub tests_msgs_from_maxmin my @msgs ; + my $mysync = { } ; # no maxage nor minage - @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + @msgs = msgs_from_maxmin( $mysync, [ '1', '2' ], [ '2', '3' ] ) ; is_deeply( [ ], \@msgs , 'msgs_from_maxmin: no maxage nor minage => empty result' ) ; # maxage alone - $maxage = $NUMBER_200 ; - @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + $mysync->{ maxage } = $NUMBER_200 ; + @msgs = msgs_from_maxmin( $mysync, [ '1', '2' ], [ '2', '3' ] ) ; is_deeply( [ '1', '2' ], \@msgs , 'msgs_from_maxmin: maxage++' ) ; # maxage > minage -> intersection - $minage = $NUMBER_100 ; - @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + $mysync->{ minage } = $NUMBER_100 ; + @msgs = msgs_from_maxmin( $mysync, [ '1', '2' ], [ '2', '3' ] ) ; is_deeply( [ '2' ], \@msgs , 'msgs_from_maxmin: -maxage++minage-' ) ; # maxage < minage -> union - $minage = $NUMBER_300 ; - @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + $mysync->{ minage } = $NUMBER_300 ; + @msgs = msgs_from_maxmin( $mysync, [ '1', '2' ], [ '2', '3' ] ) ; is_deeply( [ '1', '2', '3' ], \@msgs, 'msgs_from_maxmin: ++maxage-minage++' ) ; # minage alone - $maxage = undef ; - @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + $mysync->{ maxage } = undef ; + @msgs = msgs_from_maxmin( $mysync, [ '1', '2' ], [ '2', '3' ] ) ; is_deeply( [ '2', '3' ], \@msgs, 'msgs_from_maxmin: ++minage-' ) ; - note( 'Leaving tests_msgs_from_maxmin()' ) ; return ; } @@ -11225,11 +12901,11 @@ sub info_date_from_uid } -sub lastuid +sub lastuid { - my $imap = shift ; - my $folder = shift ; - my $lastuid_guess = shift ; + my $imap = shift @ARG ; + my $folder = shift @ARG ; + my $lastuid_guess = shift @ARG ; my $lastuid ; # rfc3501: The only reliable way to identify recent messages is to @@ -11246,55 +12922,70 @@ sub lastuid my $max_recent ; $max_recent = max( @recent_messages ) ; - if ( defined $max_recent and ($lastuid_guess <= $max_recent ) ) { + if ( defined $max_recent and ( $lastuid_guess <= $max_recent ) ) + { $lastuid = $max_recent ; - }else{ + } + else + { $lastuid = $lastuid_guess } return( $lastuid ) ; -} +} -sub size_filtered +sub size_filtered { - my( $h1_size, $h1_msg, $h1_fold, $h2_fold ) = @_ ; + my( $mysync, $h1_size, $h1_msg, $h1_fold, $h2_fold ) = @ARG ; $h1_size = 0 if ( ! $h1_size ) ; # null if empty or undef - if ( defined $sync->{ maxsize } and $h1_size > $sync->{ maxsize } ) { - myprint( "msg $h1_fold/$h1_msg skipped ($h1_size exceeds maxsize limit $sync->{ maxsize } bytes)\n" ) ; - $sync->{ total_bytes_skipped } += $h1_size; - $sync->{ nb_msg_skipped } += 1; + if ( defined $mysync->{ maxsize } and $h1_size > $mysync->{ maxsize } ) + { + myprint( "msg $h1_fold/$h1_msg skipped ($h1_size exceeds maxsize limit $mysync->{ maxsize } bytes)\n" ) ; + $mysync->{ total_bytes_skipped } += $h1_size; + $mysync->{ nb_msg_skipped } += 1; return( 1 ) ; } - if ( defined $minsize and $h1_size <= $minsize ) { - myprint( "msg $h1_fold/$h1_msg skipped ($h1_size smaller than minsize $minsize bytes)\n" ) ; - $sync->{ total_bytes_skipped } += $h1_size; - $sync->{ nb_msg_skipped } += 1; + if ( defined $mysync->{ minsize } and $h1_size <= $mysync->{ minsize } ) + { + myprint( "msg $h1_fold/$h1_msg skipped ($h1_size smaller than minsize $mysync->{ minsize } bytes)\n" ) ; + $mysync->{ total_bytes_skipped } += $h1_size; + $mysync->{ nb_msg_skipped } += 1; return( 1 ) ; } return( 0 ) ; -} +} -sub message_exists +sub message_exists_on_host1 { - my( $imap, $msg ) = @_ ; - return( 1 ) if not $imap->Uid( ) ; + my( $mysync, $msg, $folder ) = @ARG ; + + if ( not $mysync->{ imap1 }->Uid( ) ) + { + return( 1 ) ; + } + my $search_uid ; - ( $search_uid ) = $imap->search( "UID $msg" ) ; - #myprint( "$search ? $msg\n" ) ; - return( 1 ) if ( $search_uid eq $msg ) ; - return( 0 ) ; + ( $search_uid ) = $mysync->{ imap1 }->search( "UID $msg" ) ; + + if ( $search_uid eq $msg ) + { + $mysync->{ debug } and myprint( "msg $folder/$msg exists on host1\n" ) ; + return( 1 ) ; + } + else + { + myprint( "msg $folder/$msg does not exist on host1\n" ) ; + return( 0 ) ; + } } -# Globals -# $sync->{ total_bytes_skipped } -# $sync->{ nb_msg_skipped } -# $mysync->{ h1_nb_msg_processed } + sub stats_update_skip_message { - my $mysync = shift ; # to be used - my $h1_size = shift ; + my $mysync = shift @ARG ; # to be used + my $h1_size = shift @ARG ; $mysync->{ total_bytes_skipped } += $h1_size ; $mysync->{ nb_msg_skipped } += 1 ; @@ -11302,18 +12993,18 @@ sub stats_update_skip_message return ; } -sub copy_message +sub copy_message { # copy - my ( $mysync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) = @_ ; - ( $mysync->{ debug } or $mysync->{dry} ) - and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $mysync->{dry_message} " . eta( $mysync ) . "\n" ) ; + my ( $mysync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $cache_dir ) = @ARG ; + ( $mysync->{ debug } or $mysync->{ dry } ) + and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $mysync->{ dry_message } " . eta( $mysync ) . "\n" ) ; if ( $mysync->{dry1} ) { $mysync->{ h1_nb_msg_processed } +=1 ; - $nb_msg_skipped_dry_mode += 1 ; + $mysync->{ nb_msg_skipped_dry_mode } += 1 ; return ; } @@ -11322,7 +13013,7 @@ sub copy_message my $h1_idate = $h1_fir_ref->{$h1_msg}->{'INTERNALDATE'} || q{} ; - if ( size_filtered( $h1_size, $h1_msg, $h1_fold, $h2_fold ) ) { + if ( size_filtered( $mysync, $h1_size, $h1_msg, $h1_fold, $h2_fold ) ) { $mysync->{ h1_nb_msg_processed } +=1 ; return ; } @@ -11330,16 +13021,17 @@ sub copy_message debugsleep( $mysync ) ; myprint( "- msg $h1_fold/$h1_msg S[$h1_size] F[$h1_flags] I[$h1_idate] has RFC822.SIZE null!\n" ) if ( ! $h1_size ) ; - if ( $checkmessageexists and not message_exists( $mysync->{imap1}, $h1_msg ) ) { + if ( $mysync->{ checkmessageexists } and not message_exists_on_host1( $mysync, $h1_msg, $h1_fold ) ) + { stats_update_skip_message( $mysync, $h1_size ) ; return ; } - myprint( debugmemory( $mysync, " at C1" ) ) ; + myprint( debugmemory( $mysync, " at sub copy_message 1" ) ) ; my ( $string, $string_len ) ; ( $string_len ) = message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, \$string ) ; - myprint( debugmemory( $mysync, " at C2" ) ) ; + myprint( debugmemory( $mysync, " at sub copy_message 2" ) ) ; # not defined or empty $string if ( ( not $string ) or ( not $string_len ) ) { @@ -11349,7 +13041,8 @@ sub copy_message } # Lines too long (or not enough) => do no copy or fix - if ( ( defined $maxlinelength ) or ( defined $minmaxlinelength ) ) { + if ( defined $maxlinelength ) + { $string = linelengthstuff( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) ; if ( not defined $string ) { stats_update_skip_message( $mysync, $h1_size ) ; @@ -11357,14 +13050,14 @@ sub copy_message } } - my $h1_date = date_for_host2( $h1_msg, $h1_idate ) ; + my $h1_date = date_for_host2( $mysync, $h1_msg, $h1_idate ) ; - ( $mysync->{ debug } or $debugflags ) and + ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host1: flags init msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n" ) ; - $h1_flags = flags_for_host2( $mysync, $h1_flags, $permanentflags2 ) ; + $h1_flags = flags_for_host2( $mysync, $h1_flags, $mysync->{ permanentflags2 } ) ; - ( $mysync->{ debug } or $debugflags ) and + ( $mysync->{ debug } or $mysync->{ debugflags } ) and myprint( "Host1: flags filt msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n" ) ; $h1_date = undef if ( $h1_date eq q{} ) ; @@ -11373,43 +13066,26 @@ sub copy_message - if ( $new_id and $syncflagsaftercopy ) { - sync_flags_after_copy( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $new_id, $permanentflags2 ) ; + if ( $new_id and $mysync->{ syncflagsaftercopy } ) + { + syncflagsaftercopy( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $new_id ) ; } - myprint( debugmemory( $mysync, " at C3" ) ) ; + myprint( debugmemory( $mysync, " at sub copy_message 3" ) ) ; return $new_id ; -} +} -sub linelengthstuff +sub linelengthstuff { - my( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) = @_ ; + my( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) = @ARG ; my $maxlinelength_string = max_line_length( $string ) ; $debugmaxlinelength and myprint( "msg $h1_fold/$h1_msg maxlinelength: $maxlinelength_string\n" ) ; - if ( ( defined $minmaxlinelength ) and ( $maxlinelength_string <= $minmaxlinelength ) ) { - my $subject = subject( $string ) ; - $debugdev and myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] " - . "(Subject:[$subject]) (max line length under minmaxlinelength $minmaxlinelength bytes)\n" ) ; - return ; - } - if ( ( defined $maxlinelength ) and ( $maxlinelength_string > $maxlinelength ) ) { my $subject = subject( $string ) ; - if ( $maxlinelengthcmd ) { - $string = pipemess( $string, $maxlinelengthcmd ) ; - # string undef means something was bad. - if ( not ( defined $string ) ) { - myprint( "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] " - . "(Subject:[$subject]) could not be successfully transformed by --maxlinelengthcmd option\n" ) ; - return ; - }else{ - return $string ; - } - } myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] " . "(Subject:[$subject]) (line length exceeds maxlinelength $maxlinelength bytes)\n" ) ; return ; @@ -11425,8 +13101,6 @@ sub message_for_host2 # @skipmess # @regexmess # @pipemess -# $debugcontent -# $debug # # API current # @@ -11440,7 +13114,7 @@ sub message_for_host2 # API future # # - my ( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) = @_ ; + my ( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) = @ARG ; # abort when missing a parameter if ( ( ! $mysync ) or ( ! $h1_msg ) or ( ! $h1_fold ) or ( ! defined $h1_size ) @@ -11450,12 +13124,12 @@ sub message_for_host2 return ; } - myprint( debugmemory( $mysync, " at M1" ) ) ; + myprint( debugmemory( $mysync, " at sub message_for_host2 M1" ) ) ; - my $string_ok = $mysync->{imap1}->message_to_file( $string_ref, $h1_msg ) ; + my $string_ok = $mysync->{ imap1 }->message_to_file( $string_ref, $h1_msg ) ; - myprint( debugmemory( $mysync, " at M2" ) ) ; + myprint( debugmemory( $mysync, " at sub message_for_host2 M2" ) ) ; my $string_len = length_ref( $string_ref ) ; @@ -11464,7 +13138,7 @@ sub message_for_host2 # undef or 0 length my $error = join q{}, "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] could not be fetched: ", - $mysync->{imap1}->LastError || q{}, "\n" ; + $mysync->{ imap1 }->LastError || q{}, "\n" ; errors_incr( $mysync, $error ) ; $mysync->{ h1_nb_msg_processed } +=1 ; return ; @@ -11524,7 +13198,7 @@ sub message_for_host2 $mysync->{ debugcontent } and myprint( debugcontent( $mysync, $string_ref ) ) ; - myprint( debugmemory( $mysync, " at M3" ) ) ; + myprint( debugmemory( $mysync, " at sub message_for_host2 M3" ) ) ; return $string_len ; } @@ -11554,20 +13228,20 @@ sub debugcontent { my $mysync = shift @ARG ; if ( ! defined $mysync ) { return ; } - + my $string_ref = shift @ARG ; if ( ! defined $string_ref ) { return ; } if ( 'SCALAR' ne ref( $string_ref ) ) { return ; } - + my $string_len = length_ref( $string_ref ) ; - + my $string = join( '', q{=} x $STD_CHAR_PER_LINE, "\n", "F message content begin next line ($string_len characters long)\n", ${ $string_ref }, "\nF message content ended on previous line\n", q{=} x $STD_CHAR_PER_LINE, "\n", ) ; - + return $string ; } @@ -11592,8 +13266,8 @@ sub tests_truncmess sub truncmess { - my $string = shift ; - my $length = shift ; + my $string = shift @ARG ; + my $length = shift @ARG ; if ( not defined $string ) { return ; } if ( not defined $length ) { return $string ; } @@ -11614,7 +13288,7 @@ sub tests_message_for_host2 require_ok( "Test::MockObject" ) ; my $imapT = Test::MockObject->new( ) ; - $mysync->{imap1} = $imapT ; + $mysync->{ imap1 } = $imapT ; my $string ; $h1_msg = 1 ; @@ -11626,7 +13300,7 @@ sub tests_message_for_host2 $string_ref = \$string ; $imapT->mock( 'message_to_file', sub { - my ( $imap, $mystring_ref, $msg ) = @_ ; + my ( $imap, $mystring_ref, $msg ) = @ARG ; ${$mystring_ref} = 'blablabla' ; return length ${$mystring_ref} ; } @@ -11672,63 +13346,68 @@ sub tests_message_for_host2 is( undef, $string, q{message_for_host2: --pipemess 'true', value} ) ; } + undef @pipemess ; note( 'Leaving tests_message_for_host2()' ) ; return ; } -sub tests_labels_remove_subfolder1 -{ +sub tests_labels_remove_subfolder1 +{ note( 'Entering tests_labels_remove_subfolder1()' ) ; is( undef, labels_remove_subfolder1( ), 'labels_remove_subfolder1: no parameters => undef' ) ; - is( 'Blabla', labels_remove_subfolder1( 'Blabla' ), 'labels_remove_subfolder1: one parameter Blabla => Blabla' ) ; - is( 'Blan blue', labels_remove_subfolder1( 'Blan blue' ), 'labels_remove_subfolder1: one parameter Blan blue => Blan blue' ) ; - is( '\Bla "Blan blan" Blabla', labels_remove_subfolder1( '\Bla "Blan blan" Blabla' ), + + my $mysync = { } ; + is( 'Blabla', labels_remove_subfolder1( $mysync, 'Blabla' ), 'labels_remove_subfolder1: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_remove_subfolder1( $mysync, 'Blan blue' ), 'labels_remove_subfolder1: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_remove_subfolder1( $mysync, '\Bla "Blan blan" Blabla' ), 'labels_remove_subfolder1: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; - is( 'Bla', labels_remove_subfolder1( 'Subf/Bla', 'Subf' ), 'labels_remove_subfolder1: Subf/Bla Subf => "Bla"' ) ; + is( 'Bla', labels_remove_subfolder1( $mysync, 'Subf/Bla', 'Subf' ), 'labels_remove_subfolder1: Subf/Bla Subf => "Bla"' ) ; - is( '"\\\\Bla"', labels_remove_subfolder1( '"\\\\Bla"', 'Subf' ), 'labels_remove_subfolder1: "\\\\Bla" Subf => "\\\\Bla"' ) ; + is( '"\\\\Bla"', labels_remove_subfolder1( $mysync, '"\\\\Bla"', 'Subf' ), 'labels_remove_subfolder1: "\\\\Bla" Subf => "\\\\Bla"' ) ; - is( 'Bla Kii', labels_remove_subfolder1( 'Subf/Bla Subf/Kii', 'Subf' ), + is( 'Bla Kii', labels_remove_subfolder1( $mysync, 'Subf/Bla Subf/Kii', 'Subf' ), 'labels_remove_subfolder1: Subf/Bla Subf/Kii, Subf => "Bla" "Kii"' ) ; - is( '"\\\\Bla" Kii', labels_remove_subfolder1( '"\\\\Bla" Subf/Kii', 'Subf' ), + is( '"\\\\Bla" Kii', labels_remove_subfolder1( $mysync, '"\\\\Bla" Subf/Kii', 'Subf' ), 'labels_remove_subfolder1: "\\\\Bla" Subf/Kii Subf => "\\\\Bla" Kii' ) ; - is( '"Blan blan"', labels_remove_subfolder1( '"Subf/Blan blan"', 'Subf' ), + is( '"Blan blan"', labels_remove_subfolder1( $mysync, '"Subf/Blan blan"', 'Subf' ), 'labels_remove_subfolder1: "Subf/Blan blan" Subf => "Blan blan"' ) ; - is( '"\\\\Loo" "Blan blan" Kii', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii', 'Subf' ), + is( '"\\\\Loo" "Blan blan" Kii', labels_remove_subfolder1( $mysync, '"\\\\Loo" "Subf/Blan blan" Subf/Kii', 'Subf' ), 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii + Subf => "\\\\Loo" "Blan blan" Kii' ) ; - is( '"\\\\Inbox"', labels_remove_subfolder1( 'Subf/INBOX', 'Subf' ), + is( '"\\\\Inbox"', labels_remove_subfolder1( $mysync, 'Subf/INBOX', 'Subf' ), 'labels_remove_subfolder1: Subf/INBOX + Subf => "\\\\Inbox"' ) ; - is( '"\\\\Loo" "Blan blan" Kii "\\\\Inbox"', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX', 'Subf' ), + is( '"\\\\Loo" "Blan blan" Kii "\\\\Inbox"', labels_remove_subfolder1( $mysync, '"\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX', 'Subf' ), 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX + Subf => "\\\\Loo" "Blan blan" Kii "\\\\Inbox"' ) ; - note( 'Leaving tests_labels_remove_subfolder1()' ) ; return ; -} +} -sub labels_remove_subfolder1 -{ - my $labels = shift ; - my $subfolder1 = shift ; +sub labels_remove_subfolder1 +{ + my $mysync = shift @ARG ; + my $labels = shift @ARG ; + my $subfolder1 = shift @ARG ; + if ( not defined $mysync ) { return ; } if ( not defined $labels ) { return ; } if ( not defined $subfolder1 ) { return $labels ; } my @labels = quotewords('\s+', 1, $labels ) ; - #myprint( "@labels\n" ) ; + $mysync->{ debuglabels } and myprint( "In labels_remove_subfolder1 labels: @labels\n" ) ; my @labels_subfolder2 ; foreach my $label ( @labels ) { + # WTF is this zzzzzzzzzz test? if ( $label =~ m{zzzzzzzzzz} ) { # \Seen \Deleted ... stay the same @@ -11761,33 +13440,37 @@ sub labels_remove_subfolder1 my $labels_subfolder2 = join( ' ', sort uniq( @labels_subfolder2 ) ) ; return $labels_subfolder2 ; -} +} -sub tests_labels_remove_special -{ +sub tests_labels_remove_special +{ note( 'Entering tests_labels_remove_special()' ) ; is( undef, labels_remove_special( ), 'labels_remove_special: no parameters => undef' ) ; - is( q{}, labels_remove_special( q{} ), 'labels_remove_special: empty string => empty string' ) ; - is( q{}, labels_remove_special( '"\\\\Inbox"' ), 'labels_remove_special:"\\\\Inbox" => empty string' ) ; - is( q{}, labels_remove_special( '"\\\\Inbox" "\\\\Starred"' ), 'labels_remove_special:"\\\\Inbox" "\\\\Starred" => empty string' ) ; - is( 'Bar Foo', labels_remove_special( 'Foo Bar' ), 'labels_remove_special:Foo Bar => Bar Foo' ) ; - is( 'Bar Foo', labels_remove_special( 'Foo Bar "\\\\Inbox"' ), 'labels_remove_special:Foo Bar "\\\\Inbox" => Bar Foo' ) ; + + my $mysync = { } ; + is( q{}, labels_remove_special( $mysync, q{} ), 'labels_remove_special: empty string => empty string' ) ; + is( q{}, labels_remove_special( $mysync, '"\\\\Inbox"' ), 'labels_remove_special:"\\\\Inbox" => empty string' ) ; + is( q{}, labels_remove_special( $mysync, '"\\\\Inbox" "\\\\Starred"' ), 'labels_remove_special:"\\\\Inbox" "\\\\Starred" => empty string' ) ; + is( 'Bar Foo', labels_remove_special( $mysync, 'Foo Bar' ), 'labels_remove_special:Foo Bar => Bar Foo' ) ; + is( 'Bar Foo', labels_remove_special( $mysync, 'Foo Bar "\\\\Inbox"' ), 'labels_remove_special:Foo Bar "\\\\Inbox" => Bar Foo' ) ; note( 'Leaving tests_labels_remove_special()' ) ; return ; -} +} -sub labels_remove_special -{ - my $labels = shift ; +sub labels_remove_special +{ + my $mysync = shift @ARG ; + my $labels = shift @ARG ; + if ( not defined $mysync ) { return ; } if ( not defined $labels ) { return ; } my @labels = quotewords('\s+', 1, $labels ) ; - myprint( "labels before remove_non_folded: @labels\n" ) ; + $mysync->{ debuglabels } and myprint( "labels before labels_remove_special: @labels\n" ) ; my @labels_remove_special ; foreach my $label ( @labels ) @@ -11805,48 +13488,50 @@ sub labels_remove_special my $labels_remove_special = join( ' ', sort @labels_remove_special ) ; return $labels_remove_special ; -} +} sub tests_labels_add_subfolder2 { note( 'Entering tests_labels_add_subfolder2()' ) ; is( undef, labels_add_subfolder2( ), 'labels_add_subfolder2: no parameters => undef' ) ; - is( 'Blabla', labels_add_subfolder2( 'Blabla' ), 'labels_add_subfolder2: one parameter Blabla => Blabla' ) ; - is( 'Blan blue', labels_add_subfolder2( 'Blan blue' ), 'labels_add_subfolder2: one parameter Blan blue => Blan blue' ) ; - is( '\Bla "Blan blan" Blabla', labels_add_subfolder2( '\Bla "Blan blan" Blabla' ), + + my $mysync = { } ; + is( 'Blabla', labels_add_subfolder2( $mysync, 'Blabla' ), 'labels_add_subfolder2: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_add_subfolder2( $mysync, 'Blan blue' ), 'labels_add_subfolder2: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_add_subfolder2( $mysync, '\Bla "Blan blan" Blabla' ), 'labels_add_subfolder2: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; - is( 'Subf/Bla', labels_add_subfolder2( 'Bla', 'Subf' ), 'labels_add_subfolder2: Bla Subf => "Subf/Bla"' ) ; + is( 'Subf/Bla', labels_add_subfolder2( $mysync, 'Bla', 'Subf' ), 'labels_add_subfolder2: Bla Subf => "Subf/Bla"' ) ; - is( 'Subf/\Bla', labels_add_subfolder2( '\\\\Bla', 'Subf' ), 'labels_add_subfolder2: \Bla Subf => \Bla' ) ; + is( 'Subf/\Bla', labels_add_subfolder2( $mysync, '\\\\Bla', 'Subf' ), 'labels_add_subfolder2: \Bla Subf => \Bla' ) ; - is( 'Subf/Bla Subf/Kii', labels_add_subfolder2( 'Bla Kii', 'Subf' ), + is( 'Subf/Bla Subf/Kii', labels_add_subfolder2( $mysync, 'Bla Kii', 'Subf' ), 'labels_add_subfolder2: Bla Kii Subf => "Subf/Bla" "Subf/Kii"' ) ; - is( 'Subf/Kii Subf/\Bla', labels_add_subfolder2( '\\\\Bla Kii', 'Subf' ), + is( 'Subf/Kii Subf/\Bla', labels_add_subfolder2( $mysync, '\\\\Bla Kii', 'Subf' ), 'labels_add_subfolder2: \Bla Kii Subf => \Bla Subf/Kii' ) ; - is( '"Subf/Blan blan"', labels_add_subfolder2( '"Blan blan"', 'Subf' ), + is( '"Subf/Blan blan"', labels_add_subfolder2( $mysync, '"Blan blan"', 'Subf' ), 'labels_add_subfolder2: "Blan blan" Subf => "Subf/Blan blan"' ) ; - is( '"Subf/Blan blan" Subf/Kii Subf/\Loo', labels_add_subfolder2( '\\\\Loo "Blan blan" Kii', 'Subf' ), + is( '"Subf/Blan blan" Subf/Kii Subf/\Loo', labels_add_subfolder2( $mysync, '\\\\Loo "Blan blan" Kii', 'Subf' ), 'labels_add_subfolder2: \Loo "Blan blan" Kii + Subf => "Subf/Blan blan" Subf/Kii Subf/\Loo' ) ; # "\\Inbox" is special, add to subfolder INBOX also because Gmail will but ... - is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox"', 'Subf' ), + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( $mysync, '"\\\\Inbox"', 'Subf' ), 'labels_add_subfolder2: "\\\\Inbox" Subf => "Subf/\\\\Inbox" Subf/INBOX' ) ; # but not with INBOX folder - is( '"Subf/\\\\Inbox"', labels_add_subfolder2( '"\\\\Inbox"', 'Subf', 'INBOX' ), + is( '"Subf/\\\\Inbox"', labels_add_subfolder2( $mysync, '"\\\\Inbox"', 'Subf', 'INBOX' ), 'labels_add_subfolder2: "\\\\Inbox" Subf INBOX => "Subf/\\\\Inbox"' ) ; # two times => one time - is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox" "\\\\Inbox"', 'Subf' ), + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( $mysync, '"\\\\Inbox" "\\\\Inbox"', 'Subf' ), 'labels_add_subfolder2: "\\\\Inbox" "\\\\Inbox" Subf => "Subf/\\\\Inbox"' ) ; - is( '"Subf/\\\\Starred"', labels_add_subfolder2( '"\\\\Starred"', 'Subf' ), + is( '"Subf/\\\\Starred"', labels_add_subfolder2( $mysync, '"\\\\Starred"', 'Subf' ), 'labels_add_subfolder2: "\\\\Starred" Subf => "Subf/\\\\Starred"' ) ; note( 'Leaving tests_labels_add_subfolder2()' ) ; @@ -11855,10 +13540,12 @@ sub tests_labels_add_subfolder2 sub labels_add_subfolder2 { - my $labels = shift ; - my $subfolder2 = shift ; + my $mysync = shift @ARG ; + my $labels = shift @ARG ; + my $subfolder2 = shift @ARG ; my $h1_folder = shift || q{} ; + if ( not defined $mysync ) { return ; } if ( not defined $labels ) { return ; } if ( not defined $subfolder2 ) { return $labels ; } @@ -11869,7 +13556,7 @@ sub labels_add_subfolder2 } my @labels = uniq( quotewords('\s+', 1, $labels ) ) ; - myprint( "labels before subfolder2: @labels\n" ) ; + $mysync->{ debuglabels } and myprint( "labels before subfolder2: @labels\n" ) ; my @labels_subfolder2 ; @@ -11936,7 +13623,7 @@ sub tests_labels } ) ; $myimap->mock( 'Debug' , sub { } ) ; - $myimap->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + $myimap->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @ARG ) } ) ; # real one is( undef, labels( $myimap ), 'labels: one parameter => undef' ) ; is( '\Seen Blabla', labels( $myimap, '1' ), 'labels: $mysync UID_1 => \Seen Blabla' ) ; @@ -11984,7 +13671,7 @@ sub tests_synclabels } ) ; $myimap1->mock( 'Debug', sub { } ) ; - $myimap1->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + $myimap1->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @ARG ) } ) ; # real one my $myimap2 = Test::MockObject->new( ) ; @@ -11995,8 +13682,8 @@ sub tests_synclabels ) ; - $mysync->{imap1} = $myimap1 ; - $mysync->{imap2} = $myimap2 ; + $mysync->{ imap1 } = $myimap1 ; + $mysync->{ imap2 } = $myimap2 ; is( undef, synclabels( $mysync ), 'synclabels: fresh $mysync => undef' ) ; @@ -12008,7 +13695,7 @@ sub tests_synclabels } -sub synclabels +sub synclabels { my( $mysync, $uid1, $uid2 ) = @ARG ; @@ -12018,23 +13705,23 @@ sub synclabels my $myimap1 = $mysync->{ 'imap1' } || return ; my $myimap2 = $mysync->{ 'imap2' } || return ; - $mysync->{debuglabels} and $myimap1->Debug( 1 ) ; + $mysync->{ debuglabels } and $myimap1->Debug( 1 ) ; my $labels1 = labels( $myimap1, $uid1 ) ; - $mysync->{debuglabels} and $myimap1->Debug( 0 ) ; - $mysync->{debuglabels} and myprint( "Host1 labels: $labels1\n" ) ; + $mysync->{ debuglabels } and $myimap1->Debug( 0 ) ; + $mysync->{ debuglabels } and myprint( "Host1 labels: $labels1\n" ) ; if ( $mysync->{ subfolder1 } and $labels1 ) { - $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; - $mysync->{debuglabels} and myprint( "Host1 labels with subfolder1: $labels1\n" ) ; + $labels1 = labels_remove_subfolder1( $mysync, $labels1, $mysync->{ subfolder1 } ) ; + $mysync->{ debuglabels } and myprint( "Host1 labels without subfolder1: $labels1\n" ) ; } if ( $mysync->{ subfolder2 } and $labels1 ) { - $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 } ) ; - $mysync->{debuglabels} and myprint( "Host1 labels with subfolder2: $labels1\n" ) ; + $labels1 = labels_add_subfolder2( $mysync, $labels1, $mysync->{ subfolder2 } ) ; + $mysync->{ debuglabels } and myprint( "Host1 labels with subfolder2: $labels1\n" ) ; } my $store ; @@ -12077,7 +13764,7 @@ sub tests_resynclabels } ) ; $myimap2->mock( 'Debug', sub { } ) ; - $mysync->{imap2} = $myimap2 ; + $mysync->{ imap2 } = $myimap2 ; is( 1, resynclabels( $mysync, 11, 22, $h1_fir_ref, $h2_fir_ref ), 'resynclabels: $mysync UID_1 UID_2 labels are not equal => store => 1' ) ; @@ -12092,7 +13779,8 @@ sub resynclabels { my( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref, $h1_folder ) = @ARG ; - if ( not all_defined( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref ) ) { + if ( not all_defined( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref ) ) + { return ; } @@ -12101,13 +13789,14 @@ sub resynclabels if ( $mysync->{ subfolder1 } and $labels1 ) { - $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; + $labels1 = labels_remove_subfolder1( $mysync, $labels1, $mysync->{ subfolder1 } ) ; + $mysync->{ debuglabels } and myprint( "Host1 labels without subfolder1: $labels1\n" ) ; } if ( $mysync->{ subfolder2 } and $labels1 ) { - $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 }, $h1_folder ) ; - $labels2 = labels_remove_special( $labels2 ) ; + $labels1 = labels_add_subfolder2( $mysync, $labels1, $mysync->{ subfolder2 }, $h1_folder ) ; + $labels2 = labels_remove_special( $mysync, $labels2 ) ; } $mysync->{ debuglabels } and myprint( "Host1 labels fixed: $labels1\n" ) ; $mysync->{ debuglabels } and myprint( "Host2 labels : $labels2\n" ) ; @@ -12122,9 +13811,9 @@ sub resynclabels elsif ( not $mysync->{ dry } ) { # sync needed - $mysync->{debuglabels} and $mysync->{imap2}->Debug( 1 ) ; - $store = $mysync->{imap2}->store( $uid2, "X-GM-LABELS ($labels1)" ) ; - $mysync->{debuglabels} and $mysync->{imap2}->Debug( 0 ) ; + $mysync->{debuglabels} and $mysync->{ imap2 }->Debug( 1 ) ; + $store = $mysync->{ imap2 }->store( $uid2, "X-GM-LABELS ($labels1)" ) ; + $mysync->{debuglabels} and $mysync->{ imap2 }->Debug( 0 ) ; } return $store ; @@ -12159,7 +13848,7 @@ sub uniq sub length_ref { - my $string_ref = shift ; + my $string_ref = shift @ARG ; my $string_len = defined ${ $string_ref } ? length( ${ $string_ref } ) : q{} ; # length or empty string return $string_len ; } @@ -12181,33 +13870,35 @@ sub tests_length_ref return ; } -sub date_for_host2 +sub date_for_host2 { - my( $h1_msg, $h1_idate ) = @_ ; + my( $mysync, $h1_msg, $h1_idate ) = @ARG ; my $h1_date = q{} ; - if ( $syncinternaldates ) { + if ( $mysync->{ syncinternaldates } ) + { $h1_date = $h1_idate ; - $sync->{ debug } and myprint( "internal date from host1: [$h1_date]\n" ) ; + $mysync->{ debug } and myprint( "internal date from host1: [$h1_date]\n" ) ; $h1_date = good_date( $h1_date ) ; - $sync->{ debug } and myprint( "internal date from host1: [$h1_date] (fixed)\n" ) ; + $mysync->{ debug } and myprint( "internal date from host1: [$h1_date] (fixed)\n" ) ; } - if ( $idatefromheader ) { - $h1_date = $sync->{imap1}->get_header( $h1_msg, 'Date' ) ; - $sync->{ debug } and myprint( "header date from host1: [$h1_date]\n" ) ; + if ( $mysync->{ idatefromheader } ) + { + $h1_date = $mysync->{ imap1 }->get_header( $h1_msg, 'Date' ) ; + $mysync->{ debug } and myprint( "header date from host1: [$h1_date]\n" ) ; $h1_date = good_date( $h1_date ) ; - $sync->{ debug } and myprint( "header date from host1: [$h1_date] (fixed)\n" ) ; + $mysync->{ debug } and myprint( "header date from host1: [$h1_date] (fixed)\n" ) ; } return( $h1_date ) ; -} +} sub subject { - my $string = shift ; + my $string = shift @ARG ; my $subject = q{} ; my $header = extract_header( $string ) ; @@ -12291,16 +13982,16 @@ EOF # sub append_message_on_host2 { - my( $mysync, $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @_ ; - myprint( debugmemory( $mysync, " at A1" ) ) ; + my( $mysync, $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @ARG ; + myprint( debugmemory( $mysync, " at sub append_message_on_host2 A1" ) ) ; my $new_id ; - if ( ! $mysync->{dry} ) { - $new_id = $mysync->{imap2}->append_string( $h2_fold, ${ $string_ref }, $h1_flags, $h1_date ) ; - myprint( debugmemory( $mysync, " at A2" ) ) ; + if ( ! $mysync->{ dry } ) { + $new_id = $mysync->{ imap2 }->append_string( $h2_fold, ${ $string_ref }, $h1_flags, $h1_date ) ; + myprint( debugmemory( $mysync, " at sub append_message_on_host2 A2" ) ) ; if ( ! defined $new_id ){ my $subject = subject( ${ $string_ref } ) ; - my $error_imap = $mysync->{imap2}->LastError || q{} ; + my $error_imap = $mysync->{ imap2 }->LastError || q{} ; my $error = "- msg $h1_fold/$h1_msg {$string_len} could not append ( Subject:[$subject], Date:[$h1_date], Size:[$h1_size], Flags:[$h1_flags] ) to folder $h2_fold: $error_imap\n" ; errors_incr( $mysync, $error ) ; $mysync->{ h1_nb_msg_processed } +=1 ; @@ -12311,7 +14002,7 @@ sub append_message_on_host2 # $new_id is an id if the IMAP server has the # UIDPLUS capability else just a ref if ( $new_id !~ m{^\d+$}x ) { - $new_id = lastuid( $mysync->{imap2}, $h2_fold, $h2_uidguess ) ; + $new_id = lastuid( $mysync->{ imap2 }, $h2_fold, $h2_uidguess ) ; } if ( $mysync->{ synclabels } ) { synclabels( $mysync, $h1_msg, $new_id ) } $h2_uidguess += 1 ; @@ -12320,8 +14011,8 @@ sub append_message_on_host2 $mysync->{ h1_nb_msg_processed } +=1 ; $mysync->{ biggest_message_transferred } = max( $string_len, $mysync->{ biggest_message_transferred } ) ; - my $time_spent = timesince( $mysync->{begin_transfer_time} ) ; - my $rate = bytes_display_string_bin( $mysync->{total_bytes_transferred} / $time_spent ) ; + my $time_spent = timesince( $mysync->{ begin_transfer_time } ) ; + my $rate = bytes_display_string_bin( $mysync->{ total_bytes_transferred } / $time_spent ) ; my $eta = eta( $mysync ) ; my $amount_transferred = bytes_display_string_bin( $mysync->{total_bytes_transferred} ) ; myprintf( "msg %s/%-19s copied to %s/%-10s %.2f msgs/s %s/s %s copied %s\n", @@ -12329,8 +14020,9 @@ sub append_message_on_host2 $amount_transferred, $eta ); sleep_if_needed( $mysync ) ; - if ( $usecache and $cacheaftercopy and $new_id =~ m{^\d+$}x ) { - $debugcache and myprint( "touch $cache_dir/${h1_msg}_$new_id\n" ) ; + if ( $mysync->{ usecache } and $mysync->{ cacheaftercopy } and $new_id =~ m{^\d+$}x ) + { + $mysync->{ debugcache } and myprint( "touch new $cache_dir/${h1_msg}_$new_id\n" ) ; touch( "$cache_dir/${h1_msg}_$new_id" ) or croak( "Couldn't touch $cache_dir/${h1_msg}_$new_id" ) ; } @@ -12343,7 +14035,7 @@ sub append_message_on_host2 } } else{ - $nb_msg_skipped_dry_mode += 1 ; + $mysync->{ nb_msg_skipped_dry_mode } += 1 ; $mysync->{ h1_nb_msg_processed } += 1 ; } @@ -12386,7 +14078,7 @@ sub tests_sleep_if_needed sub sleep_if_needed { - my( $mysync ) = shift ; + my( $mysync ) = shift @ARG ; if ( ! $mysync ) { return ; @@ -12431,7 +14123,7 @@ sub sleep_if_needed sub sleep_max_messages { # how long we have to sleep to go under max_messages_per_second - my( $nb_msg_transferred, $time_spent, $maxmessagespersecond ) = @_ ; + my( $nb_msg_transferred, $time_spent, $maxmessagespersecond ) = @ARG ; if ( ( not defined $maxmessagespersecond ) or $maxmessagespersecond <= 0 ) { return( 0 ) } ; my $sleep = ( $nb_msg_transferred / $maxmessagespersecond ) - $time_spent ; # the sleep must be positive @@ -12458,7 +14150,7 @@ sub tests_sleep_max_messages sub sleep_max_bytes { # how long we have to sleep to go under max_bytes_per_second - my( $total_bytes_to_consider, $time_spent, $maxbytespersecond ) = @_ ; + my( $total_bytes_to_consider, $time_spent, $maxbytespersecond ) = @ARG ; $total_bytes_to_consider ||= 0 ; $time_spent ||= 0 ; @@ -12490,7 +14182,7 @@ sub tests_sleep_max_bytes sub delete_message_on_host1 { - my( $mysync, $h1_fold, $expunge, @h1_msg ) = @_ ; + my( $mysync, $h1_fold, $expunge, @h1_msg ) = @ARG ; if ( ! $mysync->{ delete1 } ) { return ; } if ( ! @h1_msg ) { return ; } delete_messages_on_any( @@ -12537,7 +14229,7 @@ sub delete_messages_on_any { # $acc is not used yet, # - my( $mysync, $acc, $imap, $hostX_folder, $expunge, $split, @messages ) = @_ ; + my( $mysync, $acc, $imap, $hostX_folder, $expunge, $split, @messages ) = @ARG ; my $expunge_message = q{} ; my $dry_message = $mysync->{ dry_message } ; @@ -12553,7 +14245,7 @@ sub delete_messages_on_any { myprint( "$hostX_folder/$message marking deleted $expunge_message $dry_message\n" ) ; } - if ( ! $mysync->{dry} && @messages_part ) + if ( ! $mysync->{ dry } && @messages_part ) { my $nb_deleted = $imap->delete_message( $imap->Range( @messages_part ) ) ; if ( defined $nb_deleted ) @@ -12618,8 +14310,8 @@ sub tests_uidexpunge_or_expunge sub uidexpunge_or_expunge { - my $mysync = shift ; - my $imap = shift ; + my $mysync = shift @ARG ; + my $imap = shift @ARG ; my @messages = @ARG ; if ( ! $imap ) { return ; } ; @@ -12641,7 +14333,7 @@ sub uidexpunge_or_expunge sub eta_print { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( my $eta = eta( $mysync ) ) { myprint( "$eta\n" ) ; @@ -12681,7 +14373,7 @@ sub tests_eta sub eta { - my( $mysync ) = shift ; + my( $mysync ) = shift @ARG ; if ( ! $mysync ) { @@ -12692,7 +14384,7 @@ sub eta my $h1_nb_msg_start = $mysync->{ h1_nb_msg_start } ; my $h1_nb_processed = $mysync->{ h1_nb_msg_processed } ; - my $nb_msg_transferred = ( $mysync->{dry} ) ? $mysync->{ h1_nb_msg_processed } : $mysync->{ nb_msg_transferred } ; + my $nb_msg_transferred = ( $mysync->{ dry } ) ? $mysync->{ h1_nb_msg_processed } : $mysync->{ nb_msg_transferred } ; my $time_spent = timesince( $mysync->{ begin_transfer_time } ) ; $h1_nb_processed ||= 0 ; $h1_nb_msg_start ||= 0 ; @@ -12712,7 +14404,7 @@ sub eta sub time_remaining { - my( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) = @_ ; + my( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) = @ARG ; $nb_transferred ||= 1 ; # At least one is done (no division by zero) $h1_nb_processed ||= 0 ; @@ -12724,28 +14416,64 @@ sub time_remaining } -sub tests_time_remaining +sub tests_time_remaining { note( 'Entering tests_time_remaining()' ) ; # time_spent, nb_processed, nb_to_do_total, nb_transferred is( 0, time_remaining( ), 'time_remaining: no args -> 0' ) ; - is( 0, time_remaining( 0, 0, 0, 0 ), 'time_remaining: 0, 0, 0, 0 -> 0' ) ; - is( 1, time_remaining( 1, 1, 2, 1 ), 'time_remaining: 1, 1, 2, 1 -> 1' ) ; - is( 1, time_remaining( 9, 9, 10, 9 ), 'time_remaining: 9, 9, 10, 9 -> 1' ) ; - is( 9, time_remaining( 1, 1, 10, 1 ), 'time_remaining: 1, 1, 10, 1 -> 9' ) ; - is( 5, time_remaining( 5, 5, 10, 5 ), 'time_remaining: 5, 5, 10, 5 -> 5' ) ; + is( 0, time_remaining( 0, 0, 0, 0 ), 'time_remaining: 0, 0, 0, 0 -> 0' ) ; + is( 1, time_remaining( 1, 1, 2, 1 ), 'time_remaining: 1, 1, 2, 1 -> 1' ) ; + is( 1, time_remaining( 9, 9, 10, 9 ), 'time_remaining: 9, 9, 10, 9 -> 1' ) ; + is( 9, time_remaining( 1, 1, 10, 1 ), 'time_remaining: 1, 1, 10, 1 -> 9' ) ; + is( 5, time_remaining( 5, 5, 10, 5 ), 'time_remaining: 5, 5, 10, 5 -> 5' ) ; is( 25, time_remaining( 5, 5, 10, 0 ), 'time_remaining: 5, 5, 10, 0 -> ( 5 / 1 ) * ( 10 - 5) = 25' ) ; + + # 1 mess transferred in 5 sec, 10 mess initially, 5 processed is( 25, time_remaining( 5, 5, 10, 1 ), 'time_remaining: 5, 5, 10, 1 -> ( 5 / 1 ) * ( 10 - 5) = 25' ) ; note( 'Leaving tests_time_remaining()' ) ; return ; +} + + + + +sub tests_usecache_and_skipcrossduplicates +{ + note( 'Entering tests_usecache_and_skipcrossduplicates()' ) ; + + is( undef, usecache_and_skipcrossduplicates( ), 'usecache_and_skipcrossduplicates: no args => undef' ) ; + my $mysync = { } ; + is( undef, usecache_and_skipcrossduplicates( $mysync ), 'usecache_and_skipcrossduplicates: undef => undef' ) ; + + $mysync->{ usecache } = $mysync->{ skipcrossduplicates } = 1 ; + is( 1, usecache_and_skipcrossduplicates( $mysync ), 'usecache_and_skipcrossduplicates: usecache=skipcrossduplicates=1 => wrong' ) ; + + note( 'Leaving tests_usecache_and_skipcrossduplicates()' ) ; + return ; } +sub usecache_and_skipcrossduplicates +{ + my $mysync = shift @ARG ; + + if ( ! defined $mysync ) { return ; } + + if ( ( $mysync->{ usecache } ) and ( $mysync->{ skipcrossduplicates } ) ) + { + return 1 ; + } + + return ; +} + + + sub cache_map { - my ( $cache_files_ref, $h1_msgs_ref, $h2_msgs_ref ) = @_; + my ( $mysync, $cache_files_ref, $h1_msgs_ref, $h2_msgs_ref ) = @ARG; my ( %map1_2, %map2_1, %done2 ) ; my $h1_msgs_hash_ref = { } ; @@ -12755,7 +14483,7 @@ sub cache_map @{ $h2_msgs_hash_ref }{ @{ $h2_msgs_ref } } = ( ) ; foreach my $file ( sort @{ $cache_files_ref } ) { - $debugcache and myprint( "C12: $file\n" ) ; + $mysync->{ debugcache } and myprint( "Cache C12: $file\n" ) ; ( $uid1, $uid2 ) = match_a_cache_file( $file ) ; if ( exists( $h1_msgs_hash_ref->{ defined $uid1 ? $uid1 : q{} } ) @@ -12785,11 +14513,15 @@ sub cache_map return( \%map1_2, \%map2_1) ; } -sub tests_cache_map +sub tests_cache_map { note( 'Entering tests_cache_map()' ) ; - #$debugcache = 1 ; + + my $mysync = { } ; + + #$mysync->{ debugcache } = 1 ; + my @cache_files = qw ( 100_200 101_201 @@ -12813,7 +14545,7 @@ sub tests_cache_map my $msgs_2 = [ 242, 243, 260, 299, 377, 279, 255, 280, 231, 2301 ]; my( $c12, $c21 ) ; - ok( ( $c12, $c21 ) = cache_map( \@cache_files, $msgs_1, $msgs_2 ), 'cache_map: 02' ); + ok( ( $c12, $c21 ) = cache_map( $mysync, \@cache_files, $msgs_1, $msgs_2 ), 'cache_map: 02' ); my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; ok( 0 == compare_lists( [ 130, 142, 143, 177, 182, 1601 ], $a1 ), 'cache_map: 03' ); @@ -12826,11 +14558,11 @@ sub tests_cache_map note( 'Leaving tests_cache_map()' ) ; return ; -} +} sub cache_dir_fix { - my $cache_dir = shift ; + my $cache_dir = shift @ARG ; $cache_dir =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"\\])/\\$1/xg ; #myprint( "cache_dir_fix: $cache_dir\n" ) ; return( $cache_dir ) ; @@ -12856,7 +14588,7 @@ sub tests_cache_dir_fix sub cache_dir_fix_win { - my $cache_dir = shift ; + my $cache_dir = shift @ARG ; $cache_dir =~ s/(\[|\])/[$1]/xg ; #myprint( "cache_dir_fix_win: $cache_dir\n" ) ; return( $cache_dir ) ; @@ -12874,16 +14606,14 @@ sub tests_cache_dir_fix_win } - - -sub get_cache +sub get_cache { - my ( $cache_dir, $h1_msgs_ref, $h2_msgs_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @_; + my ( $mysync, $cache_dir, $h1_msgs_ref, $h2_msgs_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @ARG; - $debugcache and myprint( "Entering get_cache\n" ) ; + $mysync->{ debugcache } and myprint( "Entering get_cache\n" ) ; -d $cache_dir or return( undef ); # exit if cache directory doesn't exist - $debugcache and myprint( "cache_dir : $cache_dir\n" ) ; + $mysync->{ debugcache } and myprint( "cache_dir : $cache_dir\n" ) ; if ( 'MSWin32' ne $OSNAME ) { @@ -12892,19 +14622,19 @@ sub get_cache $cache_dir = cache_dir_fix_win( $cache_dir ) ; } - $debugcache and myprint( "cache_dir_fix: $cache_dir\n" ) ; + $mysync->{ debugcache } and myprint( "cache_dir_fix: $cache_dir\n" ) ; my @cache_files = bsd_glob( "$cache_dir/*" ) ; - #$debugcache and myprint( "cache_files: [@cache_files]\n" ) ; + #$mysync->{ debugcache } and myprint( "cache_files: [@cache_files]\n" ) ; - $debugcache and myprint( 'cache_files: ', scalar @cache_files , " files found\n" ) ; + $mysync->{ debugcache } and myprint( 'cache_files: ', scalar @cache_files , " files found\n" ) ; my( $cache_1_2_ref, $cache_2_1_ref ) - = cache_map( \@cache_files, $h1_msgs_ref, $h2_msgs_ref ) ; + = cache_map( $mysync, \@cache_files, $h1_msgs_ref, $h2_msgs_ref ) ; - clean_cache( \@cache_files, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; + clean_cache( $mysync, \@cache_files, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; - $debugcache and myprint( "Exiting get_cache\n" ) ; + $mysync->{ debugcache } and myprint( "Exiting get_cache\n" ) ; return( $cache_1_2_ref, $cache_2_1_ref ) ; } @@ -12913,9 +14643,10 @@ sub tests_get_cache { note( 'Entering tests_get_cache()' ) ; - ok( not( get_cache('/cache_no_exist') ), 'get_cache: /cache_no_exist' ); - ok( ( not -d 'W/tmp/cache/F1/F2' or rmtree( 'W/tmp/cache/F1/F2' ) ), 'get_cache: rmtree W/tmp/cache/F1/F2' ) ; - ok( mkpath( 'W/tmp/cache/F1/F2' ), 'get_cache: mkpath W/tmp/cache/F1/F2' ) ; + my $mysync = { } ; + ok( not( get_cache( $mysync, '/cache_no_exist') ), 'tests_get_cache: /cache_no_exist' ); + ok( ( not -d 'W/tmp/cache/F1/F2' or rmtree( 'W/tmp/cache/F1/F2' ) ), 'tests_get_cache: rmtree W/tmp/cache/F1/F2' ) ; + ok( mkpath( 'W/tmp/cache/F1/F2' ), 'tests_get_cache: mkpath W/tmp/cache/F1/F2' ) ; my @test_files_cache = ( qw( W/tmp/cache/F1/F2/100_200 @@ -12928,7 +14659,7 @@ sub tests_get_cache W/tmp/cache/F1/F2/177_777 W/tmp/cache/F1/F2/155_255 ) ) ; - ok( touch( @test_files_cache ), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ; + ok( touch( @test_files_cache ), 'tests_get_cache: touch W/tmp/cache/F1/F2/...' ) ; # on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255 @@ -12940,31 +14671,32 @@ sub tests_get_cache my $msgs_all_2 = { 242 => 0, 243 => 0, 299 => 0, 377 => 0, 777 => 0, 255 => 0 } ; my( $c12, $c21 ) ; - ok( ( $c12, $c21 ) = get_cache( 'W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' ); + ok( ( $c12, $c21 ) = get_cache( $mysync, 'W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'tests_get_cache: 02' ); my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; - ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: 03' ); - ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: 04' ); - ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242'); - ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243'); - ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file removed 100_200'); - ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file removed 101_201'); + ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'tests_get_cache: 03' ); + ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'tests_get_cache: 04' ); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'tests_get_cache: file kept 142_242'); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'tests_get_cache: file kept 143_243'); + ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'tests_get_cache: file removed 100_200'); + ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'tests_get_cache: file removed 101_201'); # test clean_cache executed - $maxage = 2 ; - ok( touch(@test_files_cache), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ; - ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' ); - ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242'); - ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243'); - ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file NOT removed 100_200'); - ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file NOT removed 101_201'); + $mysync = { } ; + $mysync->{ maxage } = 2 ; + ok( touch(@test_files_cache), 'tests_get_cache: touch W/tmp/cache/F1/F2/...' ) ; + ok( ( $c12, $c21 ) = get_cache( $mysync, 'W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'tests_get_cache: 02' ); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'tests_get_cache: file kept 142_242'); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'tests_get_cache: file kept 143_243'); + ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'tests_get_cache: file NOT removed 100_200'); + ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'tests_get_cache: file NOT removed 101_201'); # strange files - #$debugcache = 1 ; - $maxage = undef ; - ok( ( not -d 'W/tmp/cache/rr\uee' or rmtree( 'W/tmp/cache/rr\uee' )), 'get_cache: rmtree W/tmp/cache/rr\uee' ) ; - ok( mkpath( 'W/tmp/cache/rr\uee' ), 'get_cache: mkpath W/tmp/cache/rr\uee' ) ; + #$mysync->{ debugcache } = 1 ; + $mysync->{ maxage } = undef ; + ok( ( not -d 'W/tmp/cache/rr\uee' or rmtree( 'W/tmp/cache/rr\uee' )), 'tests_get_cache: rmtree W/tmp/cache/rr\uee' ) ; + ok( mkpath( 'W/tmp/cache/rr\uee' ), 'tests_get_cache: mkpath W/tmp/cache/rr\uee' ) ; @test_files_cache = ( qw( W/tmp/cache/rr\uee/100_200 @@ -12977,7 +14709,7 @@ sub tests_get_cache W/tmp/cache/rr\uee/177_777 W/tmp/cache/rr\uee/155_255 ) ) ; - ok( touch(@test_files_cache), 'get_cache: touch strange W/tmp/cache/...' ) ; + ok( touch(@test_files_cache), 'tests_get_cache: touch strange W/tmp/cache/...' ) ; # on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255 # on live: @@ -12987,23 +14719,23 @@ sub tests_get_cache $msgs_all_1 = { 120 => q{}, 142 => q{}, 143 => q{}, 144 => q{}, 177 => q{} } ; $msgs_all_2 = { 242 => q{}, 243 => q{}, 299 => q{}, 377 => q{}, 777 => q{}, 255 => q{} } ; - ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/rr\uee', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2), 'get_cache: strange path 02' ); + ok( ( $c12, $c21 ) = get_cache( $mysync, 'W/tmp/cache/rr\uee', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2), 'tests_get_cache: strange path 02' ); $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; - ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: strange path 03' ); - ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: strange path 04' ); - ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 142_242'); - ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 143_243'); - ok( ! -f 'W/tmp/cache/rr\uee/100_200', 'get_cache: strange path file removed 100_200'); - ok( ! -f 'W/tmp/cache/rr\uee/101_201', 'get_cache: strange path file removed 101_201'); + ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'tests_get_cache: strange path 03' ); + ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'tests_get_cache: strange path 04' ); + ok( -f 'W/tmp/cache/rr\uee/142_242', 'tests_get_cache: strange path file kept 142_242'); + ok( -f 'W/tmp/cache/rr\uee/142_242', 'tests_get_cache: strange path file kept 143_243'); + ok( ! -f 'W/tmp/cache/rr\uee/100_200', 'tests_get_cache: strange path file removed 100_200'); + ok( ! -f 'W/tmp/cache/rr\uee/101_201', 'tests_get_cache: strange path file removed 101_201'); note( 'Leaving tests_get_cache()' ) ; return ; -} +} sub match_a_cache_file { - my $file = shift ; + my $file = shift @ARG ; my ( $cache_uid1, $cache_uid2 ) ; return( ( undef, undef ) ) if ( ! $file ) ; @@ -13019,31 +14751,31 @@ sub tests_match_a_cache_file note( 'Entering tests_match_a_cache_file()' ) ; my ( $tuid1, $tuid2 ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( ), 'match_a_cache_file: no arg' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( ), 'tests_match_a_cache_file: no arg' ) ; ok( ! defined $tuid1 , 'match_a_cache_file: no arg 1' ) ; ok( ! defined $tuid2 , 'match_a_cache_file: no arg 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( q{} ), 'match_a_cache_file: empty arg' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( q{} ), 'tests_match_a_cache_file: empty arg' ) ; ok( ! defined $tuid1 , 'match_a_cache_file: empty arg 1' ) ; ok( ! defined $tuid2 , 'match_a_cache_file: empty arg 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '000_000' ), 'match_a_cache_file: 000_000' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '000_000' ), 'tests_match_a_cache_file: 000_000' ) ; ok( '000' eq $tuid1, 'match_a_cache_file: 000_000 1' ) ; ok( '000' eq $tuid2, 'match_a_cache_file: 000_000 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '123_456' ), 'match_a_cache_file: 123_456' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '123_456' ), 'tests_match_a_cache_file: 123_456' ) ; ok( '123' eq $tuid1, 'match_a_cache_file: 123_456 1' ) ; ok( '456' eq $tuid2, 'match_a_cache_file: 123_456 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/tmp/truc/123_456' ), 'match_a_cache_file: /tmp/truc/123_456' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/tmp/truc/123_456' ), 'tests_match_a_cache_file: /tmp/truc/123_456' ) ; ok( '123' eq $tuid1, 'match_a_cache_file: /tmp/truc/123_456 1' ) ; ok( '456' eq $tuid2, 'match_a_cache_file: /tmp/truc/123_456 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/lala123_456' ), 'match_a_cache_file: NO /lala123_456' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/lala123_456' ), 'tests_match_a_cache_file: NO /lala123_456' ) ; ok( ! $tuid1, 'match_a_cache_file: /lala123_456 1' ) ; ok( ! $tuid2, 'match_a_cache_file: /lala123_456 2' ) ; - ok( ( $tuid1, $tuid2 ) = match_a_cache_file( 'la123_456' ), 'match_a_cache_file: NO la123_456' ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( 'la123_456' ), 'tests_match_a_cache_file: NO la123_456' ) ; ok( ! $tuid1, 'match_a_cache_file: la123_456 1' ) ; ok( ! $tuid2, 'match_a_cache_file: la123_456 2' ) ; @@ -13053,15 +14785,15 @@ sub tests_match_a_cache_file sub clean_cache { - my ( $cache_files_ref, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @_ ; + my ( $mysync, $cache_files_ref, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @ARG ; - $debugcache and myprint( "Entering clean_cache\n" ) ; + $mysync->{ debugcache } and myprint( "Entering clean_cache\n" ) ; - $debugcache and myprint( map { "$_ -> " . $cache_1_2_ref->{ $_ } . "\n" } keys %{ $cache_1_2_ref } ) ; + $mysync->{ debugcache } and myprint( map { "$_ -> " . $cache_1_2_ref->{ $_ } . "\n" } keys %{ $cache_1_2_ref } ) ; foreach my $file ( @{ $cache_files_ref } ) { - $debugcache and myprint( "$file\n" ) ; + $mysync->{ debugcache } and myprint( "$file\n" ) ; my ( $cache_uid1, $cache_uid2 ) = match_a_cache_file( $file ) ; - $debugcache and myprint( "u1: $cache_uid1 u2: $cache_uid2 c12: ", $cache_1_2_ref->{ $cache_uid1 } || q{}, "\n") ; + $mysync->{ debugcache } and myprint( "u1: $cache_uid1 u2: $cache_uid2 c12: ", $cache_1_2_ref->{ $cache_uid1 } || q{}, "\n") ; # or ( ! exists( $cache_1_2_ref->{ $cache_uid1 } ) ) # or ( ! ( $cache_uid2 == $cache_1_2_ref->{ $cache_uid1 } ) ) if ( ( not defined $cache_uid1 ) @@ -13069,21 +14801,20 @@ sub clean_cache or ( not exists $h1_msgs_all_hash_ref->{ $cache_uid1 } ) or ( not exists $h2_msgs_all_hash_ref->{ $cache_uid2 } ) ) { - $debugcache and myprint( "remove $file\n" ) ; + $mysync->{ debugcache } and myprint( "remove $file\n" ) ; unlink $file or myprint( "$OS_ERROR" ) ; } } - - $debugcache and myprint( "Exiting clean_cache\n" ) ; + $mysync->{ debugcache } and myprint( "Exiting clean_cache\n" ) ; return( 1 ) ; } -sub tests_clean_cache +sub tests_clean_cache { note( 'Entering tests_clean_cache()' ) ; - ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache: rmtree W/tmp/cache/G1/G2' ) ; - ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache: mkpath W/tmp/cache/G1/G2' ) ; + ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'tests_clean_cache: rmtree W/tmp/cache/G1/G2' ) ; + ok( mkpath( 'W/tmp/cache/G1/G2' ), 'tests_clean_cache: mkpath W/tmp/cache/G1/G2' ) ; my @test_files_cache = ( qw( W/tmp/cache/G1/G2/100_200 @@ -13096,14 +14827,14 @@ sub tests_clean_cache W/tmp/cache/G1/G2/177_777 W/tmp/cache/G1/G2/155_255 ) ) ; - ok( touch(@test_files_cache), 'clean_cache: touch W/tmp/cache/G1/G2/...' ) ; + ok( touch(@test_files_cache), 'tests_clean_cache: touch W/tmp/cache/G1/G2/...' ) ; - ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 before' ); - ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 before' ); - ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 before' ); + ok( -f 'W/tmp/cache/G1/G2/100_200', 'tests_clean_cache: 100_200 before' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'tests_clean_cache: 142_242 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_277', 'tests_clean_cache: 177_277 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_377', 'tests_clean_cache: 177_377 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'tests_clean_cache: 177_777 before' ); + ok( -f 'W/tmp/cache/G1/G2/155_255', 'tests_clean_cache: 155_255 before' ); my $cache = { 142 => 242, @@ -13120,25 +14851,27 @@ sub tests_clean_cache 242 => q{}, 777 => q{}, } ; - ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache: ' ) ; + + my $mysync = { } ; + ok( clean_cache( $mysync, \@test_files_cache, $cache, $all_1, $all_2 ), 'tests_clean_cache: ' ) ; - ok( ! -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 after' ); - ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 after' ); - ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/100_200', 'tests_clean_cache: 100_200 after' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'tests_clean_cache: 142_242 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'tests_clean_cache: 177_277 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'tests_clean_cache: 177_377 after' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'tests_clean_cache: 177_777 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'tests_clean_cache: 155_255 after' ); note( 'Leaving tests_clean_cache()' ) ; return ; -} +} -sub tests_clean_cache_2 +sub tests_clean_cache_2 { note( 'Entering tests_clean_cache_2()' ) ; - ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache_2: rmtree W/tmp/cache/G1/G2' ) ; - ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache_2: mkpath W/tmp/cache/G1/G2' ) ; + ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'tests_clean_cache_2: rmtree W/tmp/cache/G1/G2' ) ; + ok( mkpath( 'W/tmp/cache/G1/G2' ), 'tests_clean_cache_2: mkpath W/tmp/cache/G1/G2' ) ; my @test_files_cache = ( qw( W/tmp/cache/G1/G2/100_200 @@ -13151,14 +14884,14 @@ sub tests_clean_cache_2 W/tmp/cache/G1/G2/177_777 W/tmp/cache/G1/G2/155_255 ) ) ; - ok( touch(@test_files_cache), 'clean_cache_2: touch W/tmp/cache/G1/G2/...' ) ; + ok( touch(@test_files_cache), 'tests_clean_cache_2: touch W/tmp/cache/G1/G2/...' ) ; - ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 before' ); - ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 before' ); - ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 before' ); - ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 before' ); + ok( -f 'W/tmp/cache/G1/G2/100_200', 'tests_clean_cache_2: 100_200 before' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'tests_clean_cache_2: 142_242 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_277', 'tests_clean_cache_2: 177_277 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_377', 'tests_clean_cache_2: 177_377 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'tests_clean_cache_2: 177_777 before' ); + ok( -f 'W/tmp/cache/G1/G2/155_255', 'tests_clean_cache_2: 155_255 before' ); my $cache = { 142 => 242, @@ -13178,19 +14911,20 @@ sub tests_clean_cache_2 } ; + my $mysync = { } ; + + ok( clean_cache( $mysync, \@test_files_cache, $cache, $all_1, $all_2 ), 'tests_clean_cache_2: ' ) ; - ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache_2: ' ) ; - - ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 after' ); - ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 after' ); - ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 after' ); - ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 after' ); + ok( -f 'W/tmp/cache/G1/G2/100_200', 'tests_clean_cache_2: 100_200 after' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'tests_clean_cache_2: 142_242 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'tests_clean_cache_2: 177_277 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'tests_clean_cache_2: 177_377 after' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'tests_clean_cache_2: 177_777 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'tests_clean_cache_2: 155_255 after' ); note( 'Leaving tests_clean_cache_2()' ) ; return ; -} +} @@ -13270,22 +15004,29 @@ sub tests_touch } -sub touch +sub touch { - my @files = @_ ; + # return 1 if it went good + # return 0 in case of failures + + my @files = @ARG ; my $failures = 0 ; - foreach my $file ( @files ) { + foreach my $file ( @files ) + { my $fh = IO::File->new ; - if ( $fh->open(">> $file" ) ) { + if ( $fh->open(">> $file" ) ) + { $fh->close ; - }else{ - myprint( "Could not open file $file in write/append mode\n" ) ; + } + else + { + #myprint( "Could not open file $file in write/append mode\n" ) ; $failures++ ; } } - return( ! $failures ); -} + return( ! $failures ) ; +} @@ -13304,7 +15045,7 @@ sub tests_tmpdir_has_colon_bug sub tmpdir_has_colon_bug { - my $path = shift ; + my $path = shift @ARG ; my $path_filtered = filter_forbidden_characters( $path ) ; if ( $path_filtered ne $path ) { @@ -13316,7 +15057,7 @@ sub tmpdir_has_colon_bug sub tmpdir_fix_colon_bug { - my $mysync = shift ; + my $mysync = shift @ARG ; my $err = 0 ; if ( not (-d $mysync->{ tmpdir } and -r _ and -w _) ) { myprint( "tmpdir $mysync->{ tmpdir } is not valid\n" ) ; @@ -13382,7 +15123,7 @@ sub tests_cache_folder sub cache_folder { - my( $cache_base, $cache_dir, $h1_fold, $h2_fold ) = @_ ; + my( $cache_base, $cache_dir, $h1_fold, $h2_fold ) = @ARG ; my $sep_1 = $sync->{ h1_sep } || '/'; my $sep_2 = $sync->{ h2_sep } || '/'; @@ -13396,11 +15137,12 @@ sub cache_folder return( $cache_folder ) ; } -sub tests_filter_forbidden_characters +sub tests_filter_forbidden_characters { note( 'Entering tests_filter_forbidden_characters()' ) ; is( undef , filter_forbidden_characters( ), 'filter_forbidden_characters: no args -> undef' ) ; + is( '' , filter_forbidden_characters( '' ), 'filter_forbidden_characters: empty string -> empty string' ) ; is( 'a_b' , filter_forbidden_characters( 'a_b' ), 'filter_forbidden_characters: a_b -> a_b' ) ; is( 'a_b' , filter_forbidden_characters( 'a*b' ), 'filter_forbidden_characters: a*b -> a_b' ) ; @@ -13418,7 +15160,7 @@ sub tests_filter_forbidden_characters is( "a_b" , filter_forbidden_characters( "a\\b" ), 'filter_forbidden_characters: a\b -> a_b' ) ; is( 'a-b' , filter_forbidden_characters( 'a-b' ), 'filter_forbidden_characters: a-b -> a-b' ) ; - is( 'a__-__-__-__-__b' , filter_forbidden_characters( 'aé-è-à -ç-Öb' ), 'filter_forbidden_characters: aé-è-à -ç-Öb -> a__-__-__-__-__b' ) ; + is( 'a__-__-__-__-__b-_' , filter_forbidden_characters( 'aé-è-à-ç-Öb-&' ), 'filter_forbidden_characters: aé-è-à-ç-Öb-& -> a__-__-__-__-__b' ) ; is( 'abcdABCDwxyzWXYZ012789' , filter_forbidden_characters( 'abcdABCDwxyzWXYZ012789' ), 'filter_forbidden_characters: abcdABCDwxyzWXYZ012789 -> abcdABCDwxyzWXYZ012789' ) ; @@ -13426,22 +15168,67 @@ sub tests_filter_forbidden_characters note( 'Leaving tests_filter_forbidden_characters()' ) ; return ; -} +} -sub filter_forbidden_characters + +sub filter_forbidden_characters { - my $string = shift ; + my $string = shift @ARG ; if ( ! defined $string ) { return ; } - $string =~ s{[\Q*|?:"<>' \E\t\r\n\\]}{_}xg ; + $string =~ s{[\Q*|?:"<>' &\E\t\r\n\\]}{_}xg ; # replace all non-ascii and control characters by _ $string =~ s/[[:^ascii:][:cntrl:]]/_/xg ; #myprint( "[$string]\n" ) ; return( $string ) ; +} + + + +sub tests_filter_forbidden_characters_cgi_context +{ + note( 'Entering tests_filter_forbidden_characters()' ) ; + + is( undef , filter_forbidden_characters_cgi_context( ), 'tests_filter_forbidden_characters_cgi_context: no args -> undef' ) ; + is( '' , filter_forbidden_characters_cgi_context( '' ), 'tests_filter_forbidden_characters_cgi_context: empty string -> empty string' ) ; + + is( 'a______________b', filter_forbidden_characters_cgi_context( q{a*|?:(){}[]"<>'b} ), q{tests_filter_forbidden_characters_cgi_context: a*|?:"<>'b -> a________b} ) ; + + + is( 'a_b_' , filter_forbidden_characters_cgi_context( 'a b ' ), 'tests_filter_forbidden_characters_cgi_context: "a b " -> "a_b_"' ) ; + + is( 'a_b' , filter_forbidden_characters_cgi_context( "a\tb" ), 'tests_filter_forbidden_characters_cgi_context: a\tb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters_cgi_context( "a\rb" ), 'tests_filter_forbidden_characters_cgi_context: a\rb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters_cgi_context( "a\nb" ), 'tests_filter_forbidden_characters_cgi_context: a\nb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters_cgi_context( "a\\b" ), 'tests_filter_forbidden_characters_cgi_context: a\b -> a_b' ) ; + + is( 'a-b' , filter_forbidden_characters_cgi_context( 'a-b' ), 'tests_filter_forbidden_characters_cgi_context: a-b -> a-b' ) ; + is( 'a__-__-__-__-__b' , filter_forbidden_characters_cgi_context( 'aé-è-à-ç-Öb' ), 'tests_filter_forbidden_characters_cgi_context: aé-è-à-ç-Öb -> a__-__-__-__-__b' ) ; + + is( 'abcd_ABCDwxyz-WXYZ012789' , filter_forbidden_characters_cgi_context( 'abcd_ABCDwxyz-WXYZ012789' ), + 'tests_filter_forbidden_characters_cgi_context: abcd_ABCDwxyz-WXYZ012789 -> abcd_ABCDwxyz-WXYZ012789' ) ; + + note( 'Leaving tests_filter_forbidden_characters()' ) ; + return ; } + + +sub filter_forbidden_characters_cgi_context +{ + my $string = shift @ARG ; + + if ( ! defined $string ) { return ; } + + $string =~ s{[^a-zA-Z0-9_-]}{_}xg ; + + #myprint( "[$string]\n" ) ; + return( $string ) ; +} + + sub tests_convert_sep_to_slash { note( 'Entering tests_convert_sep_to_slash()' ) ; @@ -13461,7 +15248,7 @@ sub tests_convert_sep_to_slash sub convert_sep_to_slash { - my ( $folder, $sep ) = @_ ; + my ( $folder, $sep ) = @ARG ; $folder =~ s{\Q$sep\E}{/}xg ; return( $folder ) ; @@ -13997,11 +15784,11 @@ EOM # Change 8bit characters in whole email to X characters @regexmess = ( 's{[\x80-\xff]}{X}gxms' ) ; - is( 'X-8bit: kaka 1 XX kiki', regexmess('X-8bit: kaka 1 ¤ kiki'), 'regexmess: 1 Change 8bit characters in whole email to X characters'); + is( 'X-8bit: kaka 1 XX kiki', regexmess('X-8bit: kaka 1 ¤ kiki'), 'regexmess: 1 Change 8bit characters in whole email to X characters'); # Same change but using tr @regexmess = ( 'tr [\x80-\xff] [X]' ) ; - is( 'X-8bit: kaka 1 XXXX kiki', regexmess('X-8bit: kaka 1 ¤£ kiki'), 'regexmess: 2 Change 8bit characters in whole email to X characters, using tr'); + is( 'X-8bit: kaka 1 XXXX kiki', regexmess('X-8bit: kaka 1 ¤£ kiki'), 'regexmess: 2 Change 8bit characters in whole email to X characters, using tr'); @@ -14222,7 +16009,42 @@ caca\r Hello,\r Bye.\r EOM - ), 'regexmess: 8 remove buggy X-Spam-Report: across several lines, final header, with \r'); + ), 'regexmess: 8 remove buggy X-Spam-Report: across several lines, final header, with \r') ; + + + @regexmess = ( 's{\A(.*?(?! ^$))\n^Date:(.*?$)}{$1}gxms', 's{\A(.*?(?! ^$))^Received:(.*?);(.*?)$}{$1Received:$2;$3\nDate:$3}gxms' ) ; + is( +<<'EOM' +Haha: Couliii +Received: from smtp.lamiral.info (smtp.lamiral.info [91.121.221.224]) + by lamiral.info (Postfix) with ESMTPS id E065A5C0010 + for ; Sat, 28 Sep 2024 10:25:52 +0200 (CEST) +Date: Sat, 28 Sep 2024 10:25:52 +0200 (CEST) +Received: from idf.news.ffme.fr (idf.news.ffme.fr [185.245.221.25]) + by smtp.lamiral.info (Postfix) with ESMTPS id BAB2A3287BB2 + for ; Sat, 28 Sep 2024 08:25:51 +0000 (UTC) +From: + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +Haha: Couliii +Received: from smtp.lamiral.info (smtp.lamiral.info [91.121.221.224]) + by lamiral.info (Postfix) with ESMTPS id E065A5C0010 + for ; Sat, 28 Sep 2024 10:25:52 +0200 (CEST) +Received: from idf.news.ffme.fr (idf.news.ffme.fr [185.245.221.25]) + by smtp.lamiral.info (Postfix) with ESMTPS id BAB2A3287BB2 + for ; Sat, 28 Sep 2024 08:25:51 +0000 (UTC) +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + ), 'regexmess: date replaced by first received date' ) ; + undef @regexmess ; @@ -14232,7 +16054,7 @@ EOM sub regexmess { - my ( $string ) = @_ ; + my ( $string ) = @ARG ; foreach my $regexmess ( @regexmess ) { $sync->{ debug } and myprint( "eval \$string =~ $regexmess\n" ) ; my $ret = eval "\$string =~ $regexmess ; 1" ; @@ -14544,7 +16366,7 @@ EOM sub skipmess { - my ( $string ) = @_ ; + my ( $string ) = @ARG ; my $match ; #myprint( "$string\n" ) ; foreach my $skipmess ( @skipmess ) { @@ -14577,7 +16399,7 @@ sub tests_bytes_display_string_bin is( '0.010 KiB', bytes_display_string_bin( 10 ), 'bytes_display_string_bin: 10 => 0.010 KiB' ) ; is( '0.976 KiB', bytes_display_string_bin( 999 ), 'bytes_display_string_bin: 999 => 0.976 KiB' ) ; note( bytes_display_string_bin( 999 ) ) ; - + is( '0.999 KiB', bytes_display_string_bin( 1023 ), 'bytes_display_string_bin: 1023 => 0.999 KiB' ) ; note( bytes_display_string_bin( 1023 ) ) ; is( '1.000 KiB', bytes_display_string_bin( 1024 ), 'bytes_display_string_bin: 1024 => 1.000 KiB' ) ; @@ -14586,25 +16408,25 @@ sub tests_bytes_display_string_bin is( '9.999 KiB', bytes_display_string_bin( 10_239 ), 'bytes_display_string_bin: 10_239 => 9.999 KiB' ) ; note( bytes_display_string_bin( 10_239 ) ) ; - + is( '10.000 KiB', bytes_display_string_bin( 10_240 ), 'bytes_display_string_bin: 10_240 => 10.000 KiB' ) ; note( bytes_display_string_bin( 10_240 ) ) ; - + is( '999.999 KiB', bytes_display_string_bin( 1_023_999 ), 'bytes_display_string_bin: 1_023_999 => 999.999 KiB' ) ; note( bytes_display_string_bin( 1_023_999 ) ) ; - + is( '0.977 MiB', bytes_display_string_bin( 1_024_000 ), 'bytes_display_string_bin: 1_024_000 => 0.977 MiB' ) ; note( bytes_display_string_bin( 1_024_000 ) ) ; - + is( '0.999 MiB', bytes_display_string_bin( 1_047_527 ), 'bytes_display_string_bin: 1_047_527 => 0.999 MiB' ) ; - note( bytes_display_string_bin( 1_047_527 ) ) ; - + note( bytes_display_string_bin( 1_047_527 ) ) ; + is( '0.999 MiB', bytes_display_string_bin( 1_048_051 ), 'bytes_display_string_bin: 1_048_051 => 0.999 MiB' ) ; note( bytes_display_string_bin( 1_048_051 ) ) ; - + is( '1.000 MiB', bytes_display_string_bin( 1_048_052 ), 'bytes_display_string_bin: 1_048_052 => 1.000 MiB' ) ; note( bytes_display_string_bin( 1_048_052 ) ) ; - + is( '1.000 MiB', bytes_display_string_bin( 1_048_575 ), 'bytes_display_string_bin: 1_048_575 => 1.000 MiB' ) ; is( '1.000 MiB', bytes_display_string_bin( 1_048_576 ), 'bytes_display_string_bin: 1_048_576 => 1.000 MiB' ) ; @@ -14631,7 +16453,7 @@ sub tests_bytes_display_string_bin sub bytes_display_string_bin { - my ( $bytes ) = @_ ; + my ( $bytes ) = @ARG ; my $readable_value = q{} ; @@ -14682,12 +16504,12 @@ sub tests_bytes_display_string_dec is( '1 bytes', bytes_display_string_dec( 1 ), 'bytes_display_string_dec: 1 => 1 bytes' ) ; is( '10 bytes', bytes_display_string_dec( 10 ), 'bytes_display_string_dec: 10 => 10 bytes' ) ; is( '999 bytes', bytes_display_string_dec( 999 ), 'bytes_display_string_dec: 999 => 999 bytes' ) ; - + is( '1.000 KB', bytes_display_string_dec( 1000 ), 'bytes_display_string_dec: 1000 => 1.000 KB' ) ; is( '1.001 KB', bytes_display_string_dec( 1001 ), 'bytes_display_string_dec: 1000 => 1.1001 KB' ) ; - + is( '999.999 KB', bytes_display_string_dec( 999_999 ), 'bytes_display_string_dec: 999_999 => 999.999 KB' ) ; - + is( '1.000 MB', bytes_display_string_dec( 1_000_000 ), 'bytes_display_string_dec: 1_000_000 => 1.000 MB' ) ; is( '1.000 MB', bytes_display_string_dec( 1_000_500 ), 'bytes_display_string_dec: 1_000_500 => 1.000 MB' ) ; is( '1.001 MB', bytes_display_string_dec( 1_000_501 ), 'bytes_display_string_dec: 1_000_501 => 1.001 MB' ) ; @@ -14717,7 +16539,7 @@ sub tests_bytes_display_string_dec sub bytes_display_string_dec { - my ( $bytes ) = @_ ; + my ( $bytes ) = @ARG ; my $readable_value = q{} ; @@ -14779,7 +16601,7 @@ sub tests_useheader_suggestion sub useheader_suggestion { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! defined $mysync->{ h1_nb_msg_noheader } ) { return ; @@ -14795,36 +16617,56 @@ sub useheader_suggestion return ; } -sub do_and_print_stats -{ - my $mysync = shift ; - if ( ! $mysync->{can_do_stats} ) { +sub timeend_str +{ + my $timeend = shift @ARG ; # = time + my $timeend_str = localtimez( $timeend ) ; + return( $timeend_str ) ; +} + +sub timediff +{ + my $mysync = shift @ARG ; + my $timeend = shift @ARG ; # = time + my $timediff = $timeend - $mysync->{ timestart } ; + return( $timediff ) ; +} + + +sub do_and_print_stats +{ + my $mysync = shift @ARG ; + + if ( ! $mysync->{ can_do_stats } ) { return ; } - my $timeend = time ; - my $timediff = $timeend - $mysync->{timestart} ; - - my $timeend_str = localtimez( $timeend ) ; + my $timeend = time ; + my $timediff = timediff( $mysync, $timeend ) ; + my $timeend_str = timeend_str( $timeend ) ; my $cpu_time = cpu_time( $mysync ) ; my $cpu_percent = cpu_percent( $mysync, $cpu_time, $timediff ) ; my $cpu_percent_global = cpu_percent_global( $mysync, $cpu_percent ) ; - my $memory_consumption_at_end = memory_consumption( ) || 0 ; + my $memory_consumption_at_end = memory_consumption_of_myself( ) || 0 ; my $memory_consumption_at_start = $mysync->{ memory_consumption_at_start } || 0 ; my $memory_ratio = ( $mysync->{ biggest_message_transferred } ) ? mysprintf( '%.1f', $memory_consumption_at_end / $mysync->{ biggest_message_transferred } ) : 'NA' ; - # my $useheader_suggestion = useheader_suggestion( $mysync ) ; myprint( "++++ Statistics\n" ) ; myprint( "Transfer started on : $mysync->{ timestart_str }\n" ) ; myprint( "Transfer ended on : $timeend_str\n" ) ; myprintf( "Transfer time : %.1f sec\n", $timediff ) ; myprint( "Folders synced : $h1_folders_wanted_ct/$h1_folders_wanted_nb synced\n" ) ; + myprint( "Folders deleted on host2 : $mysync->{ deleted_folders_host2 } $mysync->{ dry_message }\n" ) ; + if ( $mysync->{ deleted_folders_host2_failures } ) + { + myprint( "Folders failed to be deleted on host2 : $mysync->{ deleted_folders_host2_failures }\n" ) ; + } myprint( "Messages transferred : $mysync->{ nb_msg_transferred } " ) ; - myprint( "(could be $nb_msg_skipped_dry_mode without dry mode)" ) if ( $mysync->{dry} ) ; + myprint( "\t(could be $mysync->{ nb_msg_skipped_dry_mode } without --dry mode)" ) if ( $mysync->{ dry } ) ; myprint( "\n" ) ; myprint( "Messages skipped : $mysync->{ nb_msg_skipped }\n" ) ; myprint( "Messages found duplicate on host1 : $mysync->{ acc1 }->{ nb_msg_duplicate }\n" ) ; @@ -14847,44 +16689,457 @@ sub do_and_print_stats $timediff ||= 1 ; # No division per 0 myprintf("Message rate : %.1f messages/s\n", $mysync->{nb_msg_transferred} / $timediff ) ; myprintf("Average bandwidth rate : %.1f KiB/s\n", $mysync->{total_bytes_transferred} / $KIBI / $timediff ) ; - myprint( "Reconnections to host1 : $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; - myprint( "Reconnections to host2 : $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; - myprintf("Memory consumption at the end : %.1f MiB (started with %.1f MiB)\n", + myprint( "Reconnections to host1 : $mysync->{ imap1 }->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + myprint( "Reconnections to host2 : $mysync->{ imap2 }->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + + myprintf("Memory consumption at the end : %.1f MiB (*time %.1f MiB*h) (started with %.1f MiB)\n", $memory_consumption_at_end / $KIBI / $KIBI, + memory_consumption_surface( $mysync, $memory_consumption_at_end, $timediff ), $memory_consumption_at_start / $KIBI / $KIBI ) ; + myprint( "Load end is : " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $mysync->{cpu_number} cores\n" ) ; myprint( "CPU time and %cpu : $cpu_time sec $cpu_percent %cpu $cpu_percent_global %allcpus\n" ) ; myprintf("Biggest message transferred : %s bytes (%s)\n", $mysync->{ biggest_message_transferred }, bytes_display_string_bin( $mysync->{ biggest_message_transferred } ) ) ; myprint( "Memory/biggest message ratio : $memory_ratio\n" ) ; - if ( $mysync->{ foldersizesatend } and $mysync->{ foldersizes } ) { - - - my $nb_msg_start_diff = diff_or_NA( $mysync->{ h2_nb_msg_start }, $mysync->{ h1_nb_msg_start } ) ; - my $bytes_start_diff = diff_or_NA( $mysync->{ h2_bytes_start }, $mysync->{ h1_bytes_start } ) ; + if ( $mysync->{ foldersizesatend } and $mysync->{ foldersizes } ) + { + my $nb_msg_start_diff = diff_or_NA( $mysync->{ h2_nb_msg_start }, $mysync->{ h1_nb_msg_start } ) ; + my $bytes_start_diff = diff_or_NA( $mysync->{ h2_bytes_start }, $mysync->{ h1_bytes_start } ) ; myprintf("Start difference host2 - host1 : %s messages, %s bytes (%s)\n", $nb_msg_start_diff, - $bytes_start_diff, - bytes_display_string_bin( $bytes_start_diff ) ) ; + $bytes_start_diff, + bytes_display_string_bin( $bytes_start_diff ) ) ; - my $nb_msg_end_diff = diff_or_NA( $h2_nb_msg_end, $h1_nb_msg_end ) ; - my $bytes_end_diff = diff_or_NA( $h2_bytes_end, $h1_bytes_end ) ; + my $nb_msg_end_diff = diff_or_NA( $h2_nb_msg_end, $h1_nb_msg_end ) ; + my $bytes_end_diff = diff_or_NA( $h2_bytes_end, $h1_bytes_end ) ; myprintf("Final difference host2 - host1 : %s messages, %s bytes (%s)\n", $nb_msg_end_diff, - $bytes_end_diff, - bytes_display_string_bin( $bytes_end_diff ) ) ; + $bytes_end_diff, + bytes_display_string_bin( $bytes_end_diff ) ) ; } comment_on_final_diff_in_1_not_in_2( $mysync ) ; comment_on_final_diff_in_2_not_in_1( $mysync ) ; - myprint( "Detected $mysync->{nb_errors} errors\n\n" ) ; + myprint( "Detected $mysync->{nb_errors} errors\n" ) ; myprint( $mysync->{ warn_release }, "\n" ) ; myprint( homepage( ), "\n" ) ; return ; +} + + + +sub tests_memory_consumption_surface +{ + note( 'Entering tests_memory_consumption_surface()' ) ; + + is( undef, memory_consumption_surface( ), 'memory_consumption_surface: no args => undef' ) ; + my $mysync = { } ; + is( undef, memory_consumption_surface( $mysync ), 'memory_consumption_surface: { } => undef' ) ; + is( "1.0", memory_consumption_surface( $mysync, 1_048_576, 3600 ), 'memory_consumption_surface: 1 MB 1 hour => 1' ) ; + is( "238.4", memory_consumption_surface( $mysync, 500_000_000, 1800 ), 'memory_consumption_surface: 500 MB 30 min => 238' ) ; + + + note( 'Leaving tests_memory_consumption_surface()' ) ; + return ; } +sub memory_consumption_surface +{ + my ( $mysync, $memory_consumption_at_end, $timediff ) = @ARG ; + + if ( ! all_defined( $mysync, $memory_consumption_at_end, $timediff ) ) { return } ; + + my $memory_consumption_surface = sprintf( "%.1f", $timediff * $memory_consumption_at_end / $KIBI / $KIBI / 3600 ) ; + + return $memory_consumption_surface ; +} + + + + +sub tests_email_report_message_id +{ + note( 'Entering tests_email_report_message_id()' ) ; + + local $ENV{TZ} = 'GMT' ; + + like( email_report_message_id( ), + qr{^...._.._.._.._.._.._...__\@imapsync.tk$}xms, + 'email_report_message_id: no args => ...._.._.._.._.._.._...__@imapsync.tk' ) ; + + my $mysync = { } ; + like( email_report_message_id( $mysync ), + qr{^...._.._.._.._.._.._...__\@imapsync.tk$}xms, + 'email_report_message_id: undef => ...._.._.._.._.._.._...__@imapsync.tk' ) ; + + $mysync->{ timestart } = 1357902468.531 ; + like( + email_report_message_id( $mysync ), + qr{^2013_01_11_\d\d_07_48_530__\@imapsync.tk$}xms, + 'email_report_message_id: 1357902468.531 => 2013_01_11_\d\d_07_48_530__@imapsync.tk' ) ; + + $mysync->{ user1 } = 'sarah' ; + $mysync->{ user2 } = 'haras' ; + $mysync->{ timestart } = 1357902468.531 ; + like( + email_report_message_id( $mysync ), + qr{^2013_01_11_\d\d_07_48_530_sarah_haras\@imapsync.tk$}xms, + 'email_report_message_id: 1357902468.531 sarah haras => 2013_01_11_\d\d_07_48_530_sarah_haras@imapsync.tk' ) ; + + $mysync->{ user1 } = 'sar@ah' ; + $mysync->{ user2 } = 'har@as' ; + $mysync->{ timestart } = 1357902468.531 ; + like( + email_report_message_id( $mysync ), + qr{2013_01_11_\d\d_07_48_530_sar_ah_har_as\@imapsync.tk}, + 'email_report_message_id: 1357902468.531 sar@ah har@as => 2013_01_11_\d\d_07_48_530_sar_ah_har_as@imapsync.tk' ) ; + + note( 'Leaving tests_email_report_message_id()' ) ; + return ; +} + + +sub email_report_message_id +{ + my $mysync = shift @ARG ; + + my $time = $mysync->{ timestart } || time ; + my $user1_filtered = $mysync->{ user1 } || '' ; + my $user2_filtered = $mysync->{ user2 } || '' ; + + # Nothing but alphanumeric characters and underscores to replace the others + $user1_filtered =~ s/[^a-zA-Z0-9]/_/g ; + $user2_filtered =~ s/[^a-zA-Z0-9]/_/g ; + + my $message_id = join( '', + year_month_day_hour_min_sec_ms( $time ), + '_', + $user1_filtered, + '_', + $user2_filtered, + '@imapsync.tk', + ) ; + return $message_id ; +} + + +sub tests_date_rfc822 +{ + note( 'Entering tests_email_report_date()' ) ; + + ok( date_rfc822( ), 'date_rfc822: no args => now: ' . date_rfc822( ) ) ; + + if ( 'MSWin32' eq $OSNAME ) + { + like( date_rfc822( 1671706800 ), qr{Thu, 22 Dec 2022 \d\d:00:00 \+0000}, 'date_rfc822: 1671706800 => Thu, 22 Dec 2022 \d\d:00:00 \+0000' ) ; + like( date_rfc822( 1671534000 ), qr{Tue, 20 Dec 2022 \d\d:00:00 \+0000}, 'date_rfc822: 1671534000 => Tue, 20 Dec 2022 \d\d:00:00 \+0000' ) ; + + } + else + { + local $ENV{TZ} = 'GMT' ; + is( 'Thu, 01 Jan 1970 00:00:00 +0000', date_rfc822( 0 ), 'date_rfc822: 0 => Thu, 01 Jan 1970 00:00:00 +0000' ) ; + is( 'Thu, 22 Dec 2022 11:00:00 +0000', date_rfc822( 1671706800 ), 'date_rfc822: 1671706800 => Thu, 22 Dec 2022 11:00:00 +0000' ) ; + } + note( 'Leaving tests_email_report_date()' ) ; + return ; +} + + + +sub date_rfc822 +{ + # Later I found Mail::IMAPCLient::Rfc822_date() + # https://metacpan.org/pod/Mail::IMAPClient#Rfc822_date + my $time = shift @ARG ; + $time = defined( $time ) ? $time : time ; + my $old_locale = POSIX::setlocale( POSIX::LC_TIME, "C" ) ; + + my $date_rfc822 ; + + if ( 'MSWin32' eq $OSNAME ) + { + $date_rfc822 = POSIX::strftime( "%a, %d %b %Y %H:%M:%S +0000", localtime( $time ) ) ; + } + else + { + $date_rfc822 = POSIX::strftime( "%a, %d %b %Y %H:%M:%S %z", localtime( $time ) ) ; + } + + POSIX::setlocale( POSIX::LC_TIME, $old_locale ) ; + return $date_rfc822 ; +} + + + +sub tests_email_report_from +{ + note( 'Entering tests_email_report_from()' ) ; + + is( 'help@imapsync.tk', email_report_from( ), 'email_report_from: no args => help@imapsync.tk' ) ; + my $mysync = { } ; + is( 'help@imapsync.tk', email_report_from( $mysync ), 'email_report_from: undef => help@imapsync.tk' ) ; + $mysync->{ email_report_from } = 'foo@example.com' ; + is( 'foo@example.com', email_report_from( $mysync ), 'email_report_from: foo@example.com => foo@example.com' ) ; + + note( 'Leaving tests_email_report_from()' ) ; + return ; +} + +sub email_report_from +{ + my $mysync = shift @ARG ; + + my $email_report_from = defined( $mysync->{ email_report_from } ) + ? $mysync->{ email_report_from } + : 'help@imapsync.tk' ; + + return $email_report_from ; +} + + +sub tests_email_report_to +{ + note( 'Entering tests_email_report_from()' ) ; + + is( 'unknown@imapsync.tk', email_report_to( ), 'email_report_to: no args => help@imapsync.tk' ) ; + my $mysync = { } ; + is( 'unknown@imapsync.tk', email_report_to( $mysync ), 'email_report_to: undef => help@imapsync.tk' ) ; + $mysync->{ user2 } = 'foo@example.com' ; + is( 'foo@example.com', email_report_to( $mysync ), 'email_report_to: foo@example.com => foo@example.com' ) ; + + note( 'Leaving tests_email_report_from()' ) ; + return ; +} + +sub email_report_to +{ + my $mysync = shift @ARG ; + + my $email_report_to = defined( $mysync->{ user2 } ) + ? $mysync->{ user2 } + : 'unknown@imapsync.tk' ; + + return $email_report_to ; +} + +sub email_report_subject +{ + my $mysync = shift @ARG ; + + my $email_report_subject = 'Imapsync transferred your account' ; + return $email_report_subject ; +} + +sub tests_email_report_body_base +{ + note( 'Entering tests_email_report_body_base()' ) ; + + is( '', email_report_body_base( ), 'email_report_body_base: no args => empty string' ) ; + my $mysync = { } ; + is( '', email_report_body_base( $mysync ), 'email_report_body_base: undef => empty string' ) ; + $mysync->{ user1 } = 'user1@example.com' ; + $mysync->{ user2 } = 'user2@example.com' ; + + note( email_report_body_base( $mysync ) ) ; + + note( 'Leaving tests_email_report_body_base()' ) ; + return ; +} + +sub email_report_body_base +{ + my $mysync = shift @ARG ; + + if( ! $mysync ) { return '' ; } + if( ! all_defined( + $mysync->{ user1 }, + $mysync->{ user2 }, + ) ) { return '' ; } + + my $email_report_body_base = <<"EOM"; +\r +\r +\r +Imapsync transfer from $mysync->{ user1 } to $mysync->{ user2 }\r +\r +\r +

Hello!

\r +

\r +Imapsync just ended the synchronization from the imap account $mysync->{ user1 } to the imap account $mysync->{ user2 }.
\r +

\r +EOM + return $email_report_body_base ; +} + +sub email_report_body_extra1 +{ + my $image = '' + .'imapsync website' + .'' + . "\r\n" + ; + return $image ; +} + + +sub email_report_body_extra2 +{ + my $image = '' + .'imapsync website' + .'' + . "\r\n" + ; + return $image ; +} + + + + + +sub email_report_html_begin +{ + return '' + . "\r\n" + ; +} + +sub email_report_html_end +{ + return '' + . "\r\n" + ; +} + +sub email_report_body_begin +{ + return '' + . "\r\n" + ; +} + +sub email_report_body_end +{ + return '' + . "\r\n" + ; +} + + +sub email_report_header +{ + my $mysync = shift @ARG ; + + my @header ; + + push( @header, join( "\r\n", + 'Message-Id: ' . email_report_message_id( $mysync ), + 'Date: ' . date_rfc822( time ), + 'From: ' . email_report_from( $mysync ), + 'To: ' . email_report_to( $mysync ), + 'Subject: ' . email_report_subject( $mysync ), + 'Content-Type: text/html', + "\r\n", + ) + ) ; + + return @header ; +} + + +sub tests_email_report +{ + note( 'Entering tests_email_report()' ) ; + + is( '', email_report( ), 'email_report: undef => empty string' ) ; + my $mysync = { } ; + $mysync->{ user1 } = 'user1@example.com' ; + $mysync->{ user2 } = 'user2@example.com' ; + + note( email_report( $mysync, "c'est extra !\r\n" ) ) ; + + note( 'Leaving tests_email_report()' ) ; + return ; +} + + +sub email_report +{ + my $mysync = shift @ARG ; + + my $extra = shift @ARG ; + + if ( ! defined $mysync ) { return '' ; } + + my @email_report = ( ) ; + + push( @email_report, email_report_header( $mysync ), + email_report_html_begin( ), + email_report_body_begin( ), + email_report_body_base( $mysync ), + $extra, + email_report_body_end( ), + email_report_html_end( ), + ) ; + + my $email_report = join( "", @email_report ) ; + + return $email_report ; +} + +sub email_report_append +{ + my $acc = shift @ARG ; + my $text = shift @ARG ; + + if ( $acc->{ imap }->IsAuthenticated && $text ) + { + my $newuid = $acc->{ imap }->append_string( 'INBOX', $text ) ; + if ( $newuid ) + { + myprint( "$acc->{ Side }: Successfully put the email final report in INBOX. Use --noemailreport" . $acc->{N} . " to avoid it.\n" ) ; + } + else + { + myprint( "$acc->{ Side }: Failed to put the email final report in INBOX. Use --noemailreport" . $acc->{N} . " to avoid it.\n" ) ; + } + return $newuid ; + } + else + { + return ; + } +} + + +sub final_emails_reports +{ + my $mysync = shift @ARG ; + + if ( condition_to_put_final_emails_reports( $mysync ) ) + { + $mysync->{ emailreport2 } and email_report_append( $mysync->{ acc2 }, email_report( $mysync, email_report_body_extra2( ) ) ) ; + $mysync->{ emailreport1 } and email_report_append( $mysync->{ acc1 }, email_report( $mysync, email_report_body_extra1( ) ) ) ; + } + + return ; +} + + +sub condition_to_put_final_emails_reports +{ + my $mysync = shift @ARG ; + + if ( $mysync->{ dry } ) { return 0 ; } + if ( $mysync->{ justfolders } ) { return 0 ; } + + return 1 ; +} + + sub diff_or_NA { my( $n1, $n2 ) = @ARG ; @@ -14985,14 +17240,14 @@ sub load_modules # Globals: $skipsize $wholeheaderifneeded sub parse_header_msg { - my ( $mysync, $imap, $m_uid, $s_heads, $s_fir, $side, $s_hash ) = @_ ; + my ( $mysync, $imap, $m_uid, $folder, $s_heads, $s_fir, $side, $s_hash ) = @ARG ; my $head = $s_heads->{$m_uid} ; my $headnum = scalar keys %{ $head } ; - $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, pass one: ", $headnum, "\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid number of headers, pass one: ", $headnum, "\n" ) ; if ( ( ! $headnum ) and ( $wholeheaderifneeded ) ){ - $mysync->{ debug } and myprint( "$side: uid $m_uid no header by parse_headers so taking whole header with BODY.PEEK[HEADER]\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid no header by parse_headers so taking whole header with BODY.PEEK[HEADER]\n" ) ; $imap->fetch($m_uid, 'BODY.PEEK[HEADER]' ) ; my $whole_header = $imap->_transaction_literals ; @@ -15000,16 +17255,14 @@ sub parse_header_msg $head = decompose_header( $whole_header ) ; $headnum = scalar keys %{ $head } ; - $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, pass two: ", $headnum, "\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid number of headers, pass two: ", $headnum, "\n" ) ; } - #myprint( Data::Dumper->Dump( [ $head, \%useheader ] ) ) ; - - my $headstr = header_construct( $mysync, $head, $side, $m_uid ) ; + my $headstr = header_construct( $mysync, $head, $side, $folder, $m_uid ) ; if ( ( ! $headstr ) and ( $mysync->{addheader} ) and ( $side eq 'Host1' ) ) { my $header = add_header( $m_uid ) ; - $mysync->{ debug } and myprint( "$side: uid $m_uid no header found so adding our own [$header]\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid no header found so adding our own [$header]\n" ) ; $headstr .= uc $header ; $s_fir->{$m_uid}->{NO_HEADER} = 1; } @@ -15037,7 +17290,7 @@ sub parse_header_msg my $num = scalar( @{ $dup_ref } ) ; push( @{ $dup_ref }, $m_uid ) ; my $keydup = "$key#$num" ; - $mysync->{ debug } and myprint( "$side: uid $m_uid sig $keydup size $size idate $idate dup @{ $dup_ref }\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid sig $keydup size $size idate $idate dup @{ $dup_ref }\n" ) ; if ( $mysync->{ syncduplicates } ) { $s_hash->{"$keydup"}{'5'} = $m_md5 ; @@ -15056,7 +17309,7 @@ sub parse_header_msg $s_hash->{"$key"}{'F'} = $flags ; $s_hash->{"$key"}{'m'} = $m_uid ; $s_hash->{"$key"}{'U'} = [ $m_uid ] ; # ? or [ ] ? - $mysync->{ debug } and myprint( "$side: uid $m_uid sig $key size $size idate $idate\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid sig $key size $size idate $idate\n" ) ; return( 1 ) ; } @@ -15064,7 +17317,7 @@ sub parse_header_msg return ; } -sub tests_header_construct +sub tests_header_construct { note( 'Entering tests_header_construct()' ) ; @@ -15073,39 +17326,38 @@ sub tests_header_construct my $head = { 'key1' => [ 'val1_key1' ] } ; - is( undef, header_construct( $mysync, $head, 'Host1', '1' ), 'header_construct: key1 val1_key1 no useheader => undef' ) ; - - $mysync->{useheader}->{ 'KEY1' } = 1 ; - is( 'KEY1: VAL1_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), 'header_construct: key1 val1_key1 => KEY1: VAL1_KEY1' ) ; + is( undef, header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 no useheader => undef' ) ; + $mysync->{ useheader_h }->{ 'KEY1' } = 1 ; + is( 'KEY1: VAL1_KEY1', header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 => KEY1: VAL1_KEY1' ) ; $head = { 'key1' => [ 'val1_key1', 'val3_key1', 'val2_key1' ] } ; - is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 val3_key1 val2_key1 => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; $head = { 'key1' => [ 'val1_key1', 'val3_key1', ' val2_key1' ] } ; - is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 val3_key1 val2_key1 => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; - $mysync->{useheader}->{ 'ALL' } = 1 ; + $mysync->{ useheader_h }->{ 'ALL' } = 1 ; - is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 val3_key1 val2_key1 useheader ALL => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; $mysync->{skipheader} = 'key1' ; - is( undef, header_construct( $mysync, $head, 'Host1', '1' ), + is( undef, header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: key1 val1_key1 val3_key1 val2_key1 useheader ALL => undef' ) ; $head = { 'key1' => [ 'val1_key1', 'val3_key1', ' val2_key1' ], 'key2' => [ 'val1_key2', 'val3_key2', ' val2_key2' ] } ; - is( 'KEY2: VAL1_KEY2KEY2: VAL2_KEY2KEY2: VAL3_KEY2', header_construct( $mysync, $head, 'Host1', '1' ), + is( 'KEY2: VAL1_KEY2KEY2: VAL2_KEY2KEY2: VAL3_KEY2', header_construct( $mysync, $head, 'Host1', 'INBOX', '1' ), 'header_construct: ... useheader ALL skipheader key1 => KEY2: VAL1_KEY2KEY2: VAL2_KEY2KEY2: VAL3_KEY2' ) ; @@ -15115,24 +17367,26 @@ sub tests_header_construct # No global in header_construct -sub header_construct +sub header_construct { - my( $mysync, $head, $side, $m_uid ) = @_ ; + my( $mysync, $head, $side, $folder, $m_uid ) = @ARG ; my @headstr ; - foreach my $h ( sort keys %{ $head } ) { - next if ( not ( exists $mysync->{useheader}->{ uc $h } ) - and ( not exists $mysync->{useheader}->{ 'ALL' } ) + foreach my $h ( sort keys %{ $head } ) + { + next if ( not ( exists $mysync->{ useheader_h }->{ uc $h } ) + and ( not exists $mysync->{ useheader_h }->{ 'ALL' } ) ) ; - foreach my $val ( @{$head->{$h}} ) { + foreach my $val ( @{ $head->{ $h } } ) + { my $H = header_line_normalize( $h, $val ) ; # show stuff in debug mode - $mysync->{ debug } and myprint( "$side uid $m_uid header [$H]", "\n" ) ; + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid header [$H]", "\n" ) ; - if ( $mysync->{skipheader} and $H =~ m/$mysync->{skipheader}/xi) { - $mysync->{ debug } and myprint( "$side uid $m_uid skipping header [$H]\n" ) ; + if ( $mysync->{ skipheader } and $H =~ m/$mysync->{skipheader}/xi) { + $mysync->{ debug } and myprint( "$side: folder/uid $folder/$m_uid skipping header [$H]\n" ) ; next ; } push @headstr, $H ; @@ -15140,12 +17394,12 @@ sub header_construct } my $headstr = join( '', sort @headstr ) || undef ; return( $headstr ) ; -} +} sub header_line_normalize { - my( $header_key, $header_val ) = @_ ; + my( $header_key, $header_val ) = @ARG ; # no 8-bit data in headers ! $header_val =~ s/[\x80-\xff]/X/xog; @@ -15191,36 +17445,82 @@ sub tests_header_line_normalize } -sub tests_firstline + +sub tests_firstline_or_string +{ + note( 'Entering tests_firstline_or_string()' ) ; + + is( undef, firstline_or_string( ), 'tests_firstline_or_string: no args => undef' ) ; + + is( q{please, give a value}, firstline_or_string( q{} ), 'tests_firstline_or_string: empty string => please, give a value' ) ; + + is( q{W/tmp/tests/noexist.txt}, firstline_or_string( 'W/tmp/tests/noexist.txt' ), 'tests_firstline_or_string: get W/tmp/tests/noexist.txt string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'tests_firstline_or_string: mkpath W/tmp/tests/' ) ; + + is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'tests_firstline_or_string: put blabla in W/tmp/tests/firstline.txt' ) ; + is( 'blabla' , firstline_or_string( 'W/tmp/tests/firstline.txt' ), 'tests_firstline_or_string: get blabla from W/tmp/tests/firstline.txt' ) ; + + + note( 'Leaving tests_firstline_or_string()' ) ; + return ; +} + + +sub firstline_or_string +{ + my $file = shift @ARG ; + + if ( ! defined $file ) { return ; } + + my $string ; + + if ( -f -r $file ) + { + myprint( "Reading first line of file $file\n" ) ; + $string = firstline( $file ) ; + } + else + { + myprint( "Taking string $file as the value since it is not a file\n" ) ; + $string = $file || 'please, give a value' ; + } + + return $string ; +} + + +sub tests_firstline { note( 'Entering tests_firstline()' ) ; - is( q{}, firstline( 'W/tmp/tests/noexist.txt' ), 'firstline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + is( q{}, firstline( 'W/tmp/tests/noexist.txt' ), 'tests_firstline: get empty string from inexisting W/tmp/tests/noexist.txt' ) ; - ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'firstline: mkpath W/tmp/tests/' ) ; + my $mysync = { } ; + ok( (-d 'W/tmp/tests/' or make_valid_directory( $mysync, 'W/tmp/tests/' ) ), 'tests_firstline: make_valid_directory W/tmp/tests/' ) ; - is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'firstline: put blabla in W/tmp/tests/firstline.txt' ) ; - is( 'blabla' , firstline( 'W/tmp/tests/firstline.txt' ), 'firstline: get blabla from W/tmp/tests/firstline.txt' ) ; + is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'tests_firstline: put blabla in W/tmp/tests/firstline.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline.txt' ), 'tests_firstline: get blabla from W/tmp/tests/firstline.txt' ) ; - is( q{} , string_to_file( q{}, 'W/tmp/tests/firstline2.txt' ), 'firstline: put empty string in W/tmp/tests/firstline2.txt' ) ; - is( q{} , firstline( 'W/tmp/tests/firstline2.txt' ), 'firstline: get empty string from W/tmp/tests/firstline2.txt' ) ; + is( q{} , string_to_file( q{}, 'W/tmp/tests/firstline2.txt' ), 'tests_firstline: put empty string in W/tmp/tests/firstline2.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline2.txt' ), 'tests_firstline: get empty string from W/tmp/tests/firstline2.txt' ) ; - is( "\n" , string_to_file( "\n", 'W/tmp/tests/firstline3.txt' ), 'firstline: put CR in W/tmp/tests/firstline3.txt' ) ; - is( q{} , firstline( 'W/tmp/tests/firstline3.txt' ), 'firstline: get empty string from W/tmp/tests/firstline3.txt' ) ; + is( "\n" , string_to_file( "\n", 'W/tmp/tests/firstline3.txt' ), 'tests_firstline: put CR in W/tmp/tests/firstline3.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline3.txt' ), 'tests_firstline: get empty string from W/tmp/tests/firstline3.txt' ) ; - is( "blabla\nTiti\n" , string_to_file( "blabla\nTiti\n", 'W/tmp/tests/firstline4.txt' ), 'firstline: put blabla\nTiti\n in W/tmp/tests/firstline4.txt' ) ; - is( 'blabla' , firstline( 'W/tmp/tests/firstline4.txt' ), 'firstline: get blabla from W/tmp/tests/firstline4.txt' ) ; + is( "blabla\nTiti\n" , string_to_file( "blabla\nTiti\n", 'W/tmp/tests/firstline4.txt' ), 'tests_firstline: put blabla\nTiti\n in W/tmp/tests/firstline4.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline4.txt' ), 'tests_firstline: get blabla from W/tmp/tests/firstline4.txt' ) ; note( 'Leaving tests_firstline()' ) ; return ; -} +} sub firstline { # extract the first line of a file (without \n) # return empty string if error or empty string - my $file = shift ; + my $file = shift @ARG ; my $line ; $line = nthline( $file, 1 ) ; @@ -15229,7 +17529,7 @@ sub firstline -sub tests_secondline +sub tests_secondline { note( 'Entering tests_secondline()' ) ; @@ -15241,10 +17541,12 @@ sub tests_secondline is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/secondline.txt' ), 'secondline: put L1\nL2\nL3\nL4\n in W/tmp/tests/secondline.txt' ) ; is( 'L2' , secondline( 'W/tmp/tests/secondline.txt' ), 'secondline: get L2 from W/tmp/tests/secondline.txt' ) ; + is( "L1\n" , string_to_file( "L1\n", 'W/tmp/tests/secondline2.txt' ), 'secondline: put L1\n in W/tmp/tests/secondline2.txt' ) ; + is( '' , secondline( 'W/tmp/tests/secondline2.txt' ), 'secondline: get empty string from W/tmp/tests/secondline2.txt' ) ; note( 'Leaving tests_secondline()' ) ; return ; -} +} sub secondline @@ -15252,7 +17554,7 @@ sub secondline # extract the second line of a file (without \n) # return empty string if error or empty string - my $file = shift ; + my $file = shift @ARG ; my $line ; $line = nthline( $file, 2 ) ; @@ -15284,8 +17586,8 @@ sub nthline # extract the nth line of a file (without \n) # return empty string if error or empty string - my $file = shift ; - my $num = shift ; + my $file = shift @ARG ; + my $num = shift @ARG ; if ( ! all_defined( $file, $num ) ) { return q{} ; } @@ -15322,12 +17624,12 @@ sub tests_file_to_array sub file_to_array { - my( $file ) = shift ; + my( $file ) = shift @ARG ; if ( ! $file ) { return ; } if ( ! -e $file ) { return ; } if ( ! -f $file ) { return ; } if ( ! -r $file ) { return ; } - + my @string ; if ( open my $FILE, '<', $file ) @@ -15367,12 +17669,12 @@ sub tests_file_to_string sub file_to_string { - my $file = shift ; + my $file = shift @ARG ; if ( ! $file ) { return ; } if ( ! -e $file ) { return ; } if ( ! -f $file ) { return ; } if ( ! -r $file ) { return ; } - + return( join q{}, file_to_array( $file ) ) ; } @@ -15385,6 +17687,7 @@ sub tests_string_to_file is( undef, string_to_file( 'lalala' ), 'string_to_file: one arg => undef' ) ; is( undef, string_to_file( 'lalala', '.' ), 'string_to_file: writing a directory => undef' ) ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'string_to_file: mkpath W/tmp/tests/' ) ; + ok( (-d 'W/tmp/tests/' and chmod( 755, 'W/tmp/tests/' ) ), 'string_to_file: chmod 755 W/tmp/tests/' ) ; is( 'lalala', string_to_file( 'lalala', 'W/tmp/tests/canbewritten' ), 'string_to_file: W/tmp/tests/canbewritten with lalala' ) ; is( q{}, string_to_file( q{}, 'W/tmp/tests/empty' ), 'string_to_file: W/tmp/tests/empty with empty string' ) ; @@ -15400,11 +17703,12 @@ sub tests_string_to_file sub string_to_file { - my( $string, $file ) = @_ ; + my( $string, $file ) = @ARG ; if( ! defined $string ) { return ; } if( ! defined $file ) { return ; } - if ( ! -e $file && ! -w dirname( $file ) ) { + if ( ! -e $file && ! -w dirname( $file ) ) + { myprint( "string_to_file: directory of $file is not writable\n" ) ; return ; } @@ -15418,26 +17722,23 @@ sub string_to_file return $string ; } -0 and <<'MULTILINE_COMMENT' ; -This is a multiline comment. -Based on David Carter discussion, to do: -* Call parameters stay the same. -* Now always "return( $string, $error )". Descriptions below. -OK * Still capture STDOUT via "1> $output_tmpfile" to finish in $string and "return( $string, $error )" -OK * Now also capture STDERR via "2> $error_tmpfile" to finish in $error and "return( $string, $error )" -OK * in case of CHILD_ERROR, return( undef, $error ) - and print $error, with folder/UID/maybeSubject context, - on console and at the end with the final error listing. Count this as a sync error. -* in case of good command, take final $string as is, unless void. In case $error with value then print it. -* in case of good command and final $string empty, consider it like CHILD_ERROR => - return( undef, $error ) and print $error, with folder/UID/maybeSubject context, - on console and at the end with the final error listing. Count this as a sync error. -MULTILINE_COMMENT -# End of multiline comment. + +# * Call parameters stay the same. +# * Now always "return( $string, $error )". Descriptions below. +# OK * Still capture STDOUT via "1> $output_tmpfile" to finish in $string and "return( $string, $error )" +# OK * Now also capture STDERR via "2> $error_tmpfile" to finish in $error and "return( $string, $error )" +# OK * in case of CHILD_ERROR, return( undef, $error ) +# and print $error, with folder/UID/maybeSubject context, +# on console and at the end with the final error listing. Count this as a sync error. +# * in case of good command, take final $string as is, unless void. In case $error with value then print it. +# * in case of good command and final $string empty, consider it like CHILD_ERROR => +# return( undef, $error ) and print $error, with folder/UID/maybeSubject context, +# on console and at the end with the final error listing. Count this as a sync error. + sub pipemess { - my ( $string, @commands ) = @_ ; + my ( $string, @commands ) = @ARG ; my $error = q{} ; foreach my $command ( @commands ) { my $input_tmpfile = "$sync->{ tmpdir }/imapsync_tmp_file.$PROCESS_ID.inp.txt" ; @@ -15469,17 +17770,22 @@ sub pipemess myprint( qq{STDERR of --pipemess "$command": $error_cmd\n} ) ; } } + #myprint( "[$string]\n" ) ; - if ( wantarray ) { - return ( $string, $error ) ; - }else{ - return $string ; + + if ( wantarray ) + { + return( $string, $error ) ; + } + else + { + return( $string ) ; } } -sub tests_pipemess +sub tests_pipemess { note( 'Entering tests_pipemess()' ) ; @@ -15490,11 +17796,11 @@ sub tests_pipemess # Windows # "type" command does not accept redirection of STDIN with < # "sort" does - ok( "nochange\n" eq pipemess( 'nochange', 'sort' ), 'pipemess: nearly no change by sort' ) ; - ok( "nochange2\n" eq pipemess( 'nochange2', qw( sort sort ) ), 'pipemess: nearly no change by sort,sort' ) ; + ok( "nochange\n" eq pipemess( 'nochange', 'sort' ), 'tests_pipemess: nearly no change by sort' ) ; + ok( "nochange2\n" eq pipemess( 'nochange2', qw( sort sort ) ), 'tests_pipemess: nearly no change by sort,sort' ) ; # command not found #diag( 'Warning and failure about cacaprout are on purpose' ) ; - ok( ! defined( pipemess( q{}, 'cacaprout' ) ), 'pipemess: command not found' ) ; + ok( ! defined( pipemess( q{}, 'cacaprout' ) ), 'tests_pipemess: command not found' ) ; } ; @@ -15504,75 +17810,75 @@ sub tests_pipemess Readonly my $NB_UNX_tests_pipemess => 25 ; skip( 'Not on Unix', $NB_UNX_tests_pipemess ) if ('MSWin32' eq $OSNAME) ; # Unix - ok( 'nochange' eq pipemess( 'nochange', 'cat' ), 'pipemess: no change by cat' ) ; + ok( 'nochange' eq pipemess( 'nochange', 'cat' ), 'tests_pipemess: no change by cat' ) ; - ok( 'nochange2' eq pipemess( 'nochange2', 'cat', 'cat' ), 'pipemess: no change by cat,cat' ) ; + ok( 'nochange2' eq pipemess( 'nochange2', 'cat', 'cat' ), 'tests_pipemess: no change by cat,cat' ) ; - ok( " 1\tnumberize\n" eq pipemess( "numberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ; - ok( " 1\tnumberize\n 2\tnumberize\n" eq pipemess( "numberize\nnumberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ; + ok( " 1\tnumberize\n" eq pipemess( "numberize\n", 'cat -n' ), 'tests_pipemess: numberize by cat -n' ) ; + ok( " 1\tnumberize\n 2\tnumberize\n" eq pipemess( "numberize\nnumberize\n", 'cat -n' ), 'tests_pipemess: numberize by cat -n' ) ; - ok( "A\nB\nC\n" eq pipemess( "A\nC\nB\n", 'sort' ), 'pipemess: sort' ) ; + ok( "A\nB\nC\n" eq pipemess( "A\nC\nB\n", 'sort' ), 'tests_pipemess: sort' ) ; # command not found #diag( 'Warning and failure about cacaprout are on purpose' ) ; - is( undef, pipemess( q{}, 'cacaprout' ), 'pipemess: command not found' ) ; + is( undef, pipemess( q{}, 'cacaprout' ), 'tests_pipemess: command not found' ) ; # success with true but no output at all - is( undef, pipemess( q{blabla}, 'true' ), 'pipemess: true but no output' ) ; + is( undef, pipemess( q{blabla}, 'true' ), 'tests_pipemess: true but no output' ) ; # failure with false and no output at all - is( undef, pipemess( q{blabla}, 'false' ), 'pipemess: false and no output' ) ; + is( undef, pipemess( q{blabla}, 'false' ), 'tests_pipemess: false and no output' ) ; # Failure since pipemess is not a real pipe, so first cat wait for standard input - is( q{blabla}, pipemess( q{blabla}, '( cat|cat ) ' ), 'pipemess: ok by ( cat|cat )' ) ; + is( q{blabla}, pipemess( q{blabla}, '( cat|cat ) ' ), 'tests_pipemess: ok by ( cat|cat )' ) ; ( $stringT, $errorT ) = pipemess( 'nochange', 'cat' ) ; - is( $stringT, 'nochange', 'pipemess: list context, no change by cat, string' ) ; - is( $errorT, q{}, 'pipemess: list context, no change by cat, no error' ) ; + is( $stringT, 'nochange', 'tests_pipemess: list context, no change by cat, string' ) ; + is( $errorT, q{}, 'tests_pipemess: list context, no change by cat, no error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', 'true' ) ; - is( $stringT, undef, 'pipemess: list context, true but no output, string' ) ; - like( $errorT, qr{\QFailure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""\E}xm, 'pipemess: list context, true but no output, error' ) ; + is( $stringT, undef, 'tests_pipemess: list context, true but no output, string' ) ; + like( $errorT, qr{\QFailure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""\E}xm, 'tests_pipemess: list context, true but no output, error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', 'false' ) ; - is( $stringT, undef, 'pipemess: list context, false and no output, string' ) ; + is( $stringT, undef, 'tests_pipemess: list context, false and no output, string' ) ; like( $errorT, qr{\QFailure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""\E}xm, - 'pipemess: list context, false and no output, error' ) ; + 'tests_pipemess: list context, false and no output, error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', '/bin/echo -n blablabla' ) ; - is( $stringT, q{blablabla}, 'pipemess: list context, "echo -n blablabla", string' ) ; - is( $errorT, q{}, 'pipemess: list context, "echo blablabla", error' ) ; + is( $stringT, q{blablabla}, 'tests_pipemess: list context, "echo -n blablabla", string' ) ; + is( $errorT, q{}, 'tests_pipemess: list context, "echo blablabla", error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ; - is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla", string' ) ; - like( $errorT, qr{blablabla"}xm, 'pipemess: list context, "no output STDERR blablabla", error' ) ; + is( $stringT, undef, 'tests_pipemess: list context, "no output STDERR blablabla", string' ) ; + like( $errorT, qr{blablabla"}xm, 'tests_pipemess: list context, "no output STDERR blablabla", error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )', 'false' ) ; - is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla then false", string' ) ; - like( $errorT, qr{blablabla"}xm, 'pipemess: list context, "no output STDERR blablabla then false", error' ) ; + is( $stringT, undef, 'tests_pipemess: list context, "no output STDERR blablabla then false", string' ) ; + like( $errorT, qr{blablabla"}xm, 'tests_pipemess: list context, "no output STDERR blablabla then false", error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', 'false', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ; - is( $stringT, undef, 'pipemess: list context, "false then STDERR blablabla", string' ) ; + is( $stringT, undef, 'tests_pipemess: list context, "false then STDERR blablabla", string' ) ; like( $errorT, qr{\QFailure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""\E}xm, - 'pipemess: list context, "false then STDERR blablabla", error' ) ; + 'tests_pipemess: list context, "false then STDERR blablabla", error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo rrrrr ; echo -n error_blablabla 3>&1 1>&2 2>&3 )' ) ; - like( $stringT, qr{rrrrr}xm, 'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", string' ) ; - like( $errorT, qr{STDERR.*error_blablabla}xm, 'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", error' ) ; + like( $stringT, qr{rrrrr}xm, 'tests_pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", string' ) ; + like( $errorT, qr{STDERR.*error_blablabla}xm, 'tests_pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", error' ) ; } ( $stringT, $errorT ) = pipemess( 'dontcare', 'cacaprout' ) ; - is( $stringT, undef, 'pipemess: list context, cacaprout not found, string' ) ; + is( $stringT, undef, 'tests_pipemess: list context, cacaprout not found, string' ) ; like( $errorT, qr{\QFailure: --pipemess command "cacaprout" ended with "0" characters exit value\E}xm, - 'pipemess: list context, cacaprout not found, error' ) ; + 'tests_pipemess: list context, cacaprout not found, error' ) ; note( 'Leaving tests_pipemess()' ) ; return ; -} +} @@ -15592,7 +17898,7 @@ sub tests_is_a_release_number sub is_a_release_number { - my $number = shift ; + my $number = shift @ARG ; if ( ! defined $number ) { return ; } return( $number =~ m{^\d+\.\d+$}xo ) ; } @@ -15631,7 +17937,7 @@ sub not_long_imapsync_version_public { #myprint( "Entering not_long_imapsync_version_public\n" ) ; - my $fake = shift ; + my $fake = shift @ARG ; if ( $fake ) { return $fake } my $val ; @@ -15688,7 +17994,7 @@ sub tests_not_long_imapsync_version_public sub check_last_release { - my $fake = shift ; + my $fake = shift @ARG ; my $public_release = not_long_imapsync_version_public( $fake ) ; $sync->{ debug } and myprint( "check_last_release: [$public_release]\n" ) ; my $inline_help_when_on = '( Use --noreleasecheck to avoid this release check. )' ; @@ -15747,7 +18053,7 @@ sub tests_imapsync_context sub imapsync_context { - my $mysync = shift ; + my $mysync = shift @ARG ; my $context = q{} ; @@ -15774,7 +18080,7 @@ sub imapsync_context sub imapsync_version { - my $mysync = shift ; + my $mysync = shift @ARG ; my $rcs = $mysync->{rcs} ; my $version ; @@ -15799,7 +18105,7 @@ sub tests_version_from_rcs sub version_from_rcs { - my $rcs = shift ; + my $rcs = shift @ARG ; if ( ! $rcs ) { return ; } my $version = 'UNKNOWN' ; @@ -15833,12 +18139,12 @@ sub imapsync_basename sub localhost_info { - my $mysync = shift ; + my $mysync = shift @ARG ; my( $infos ) = join( q{}, "Here is imapsync ", imapsync_version( $mysync ), - " on host " . hostname(), + " $PROGRAM_NAME on host " . hostname(), ", a $OSNAME system with ", - ram_memory_info( ), + ram_memory_info( $mysync ), "\n", 'with Perl ', mysprintf( '%vd ', $PERL_VERSION), @@ -15847,16 +18153,16 @@ sub localhost_info return( $infos ) ; } -sub tests_cpu_number +sub tests_cpu_number { note( 'Entering tests_cpu_number()' ) ; - is( 1, is_integer( cpu_number( ) ), "cpu_number: is_integer" ) ; - ok( 1 <= cpu_number( ), "cpu_number: 1 or more" ) ; - is( 1, cpu_number( 1 ), "cpu_number: 1 => 1" ) ; - is( 1, cpu_number( $MINUS_ONE ), "cpu_number: -1 => 1" ) ; - is( 1, cpu_number( 'lalala' ), "cpu_number: lalala => 1" ) ; - is( $NUMBER_42, cpu_number( $NUMBER_42 ), "cpu_number: $NUMBER_42 => $NUMBER_42" ) ; + is( 1, is_integer( cpu_number( ) ), "tests_cpu_number: is_integer" ) ; + ok( 1 <= cpu_number( ), "tests_cpu_number: 1 or more" ) ; + is( 1, cpu_number( 1 ), "tests_cpu_number: 1 => 1" ) ; + is( 1, cpu_number( $MINUS_ONE ), "tests_cpu_number: -1 => 1" ) ; + is( 1, cpu_number( 'lalala' ), "tests_cpu_number: lalala => 1" ) ; + is( $NUMBER_42, cpu_number( $NUMBER_42 ), "tests_cpu_number: $NUMBER_42 => $NUMBER_42" ) ; note( "cpu_number = " . cpu_number( ) . "\n" ) ; note( "hostname = " . hostname( ) . "\n" ) ; @@ -15865,15 +18171,16 @@ sub tests_cpu_number { skip( 'cpu_number on host != i005 (FreeBSD)', 1 ) ; } - is( 4, cpu_number( ), "cpu_number: on i005 (FreeBSD) => 4" ) ; + is( 4, cpu_number( ), "tests_cpu_number: on i005 (FreeBSD) => 4" ) ; } ; - + SKIP: { if ( ! ( 'petite' eq hostname() ) ) { skip( 'cpu_number on host != petite (Linux)', 1 ) ; } - is( 2, cpu_number( ), "cpu_number: on petite (Linux) => 2" ) ; + #is( 2, cpu_number( ), "tests_cpu_number: on petite (Linux) => 2" ) ; + is( 1, cpu_number( 1 ), "tests_cpu_number: on petite (Linux) => 1" ) ; } ; SKIP: { @@ -15881,34 +18188,35 @@ sub tests_cpu_number { skip( 'cpu_number on host != polarhome macosx (Darwin MacOS X 10.7.5 Lion)', 1 ) ; } - is( 2, cpu_number( ), "cpu_number: on polarhome macosx (Darwin MacOS X 10.7.5 Lion) => 2" ) ; + is( 2, cpu_number( ), "tests_cpu_number: on polarhome macosx (Darwin MacOS X 10.7.5 Lion) => 2" ) ; } ; - + SKIP: { if ( ! ( 'pcHPDV7-HP' eq hostname() ) ) { skip( 'cpu_number on host != pcHPDV7-HP (Windows 7, 64bits)', 1 ) ; } - is( 2, cpu_number( ), "cpu_number: on pcHPDV7-HP (Windows 7, 64bits) => 2" ) ; + is( 2, cpu_number( ), "tests_cpu_number: on pcHPDV7-HP (Windows 7, 64bits) => 2" ) ; } ; - + SKIP: { if ( ! ( 'CUILLERE' eq hostname() ) ) { skip( 'cpu_number on host != CUILLERE (Windows XP, 32bits)', 1 ) ; } - is( 1, cpu_number( ), "cpu_number: on CUILLERE (Windows XP, 32bits) => 1" ) ; + is( 1, cpu_number( ), "tests_cpu_number: on CUILLERE (Windows XP, 32bits) => 1" ) ; } ; - + note( 'Leaving tests_cpu_number()' ) ; return ; -} +} -sub cpu_number { +sub cpu_number +{ - my $cpu_number_forced = shift ; + my $cpu_number_forced = shift @ARG ; # Well, here 1 is better than 0 or undef my $cpu_number = 1 ; # Default value, erased if better found @@ -15919,33 +18227,33 @@ sub cpu_number { $cpu_number = $ENV{"NUMBER_OF_PROCESSORS"} ; #myprint( "Number of processors found by env var NUMBER_OF_PROCESSORS: $cpu_number\n" ) ; } - + if ( 'darwin' eq $OSNAME ) { $cpu_number = backtick( "sysctl -n hw.ncpu" ) ; chomp( $cpu_number ) ; #myprint( "Number of processors found by cmd 'sysctl -n hw.ncpu': $cpu_number\n" ) ; } - + if ( 'freebsd' eq $OSNAME ) { $cpu_number = backtick( "sysctl -n kern.smp.cpus" ) ; chomp( $cpu_number ) ; #myprint( "Number of processors found by cmd 'sysctl -n kern.smp.cpus': $cpu_number\n" ) ; } - + if ( 'linux' eq $OSNAME && -e '/proc/cpuinfo' ) { @cpuinfo = file_to_array( '/proc/cpuinfo' ) ; $cpu_number = grep { /^processor/mxs } @cpuinfo ; #myprint( "Number of processors found via /proc/cpuinfo: $cpu_number\n" ) ; } - + if ( defined $cpu_number_forced ) { $cpu_number = $cpu_number_forced ; } - + return( integer_or_1( $cpu_number ) ) ; } @@ -15965,7 +18273,7 @@ sub tests_integer_or_1 sub integer_or_1 { - my $number = shift ; + my $number = shift @ARG ; if ( is_integer( $number ) ) { return $number ; } @@ -15994,7 +18302,7 @@ sub tests_is_integer sub is_integer { - my $number = shift ; + my $number = shift @ARG ; if ( ! defined $number ) { return ; } return( $number =~ m{^\d+$}xo ) ; } @@ -16009,7 +18317,7 @@ sub tests_loadavg SKIP: { skip( 'Tests for darwin', 3 ) if ('darwin' ne $OSNAME) ; is( undef, loadavg( '/noexist' ), 'loadavg: /noexist => undef' ) ; - is_deeply( + is_deeply( [ '0.11', '0.22', '0.33' ], [ loadavg( 'vm.loadavg: { 0.11 0.22 0.33 }' ) ], 'loadavg: "vm.loadavg: { 0.11 0.22 0.33 }" => 0.11 0.22 0.33' @@ -16042,16 +18350,20 @@ sub tests_loadavg sub loadavg { - if ( 'linux' eq $OSNAME ) { + if ( 'linux' eq $OSNAME ) + { return ( loadavg_linux( @ARG ) ) ; } - if ( 'freebsd' eq $OSNAME ) { + if ( 'freebsd' eq $OSNAME ) + { return ( loadavg_freebsd( @ARG ) ) ; } - if ( 'darwin' eq $OSNAME ) { + if ( 'darwin' eq $OSNAME ) + { return ( loadavg_darwin( @ARG ) ) ; } - if ( 'MSWin32' eq $OSNAME ) { + if ( 'MSWin32' eq $OSNAME ) + { return ( loadavg_windows( @ARG ) ) ; } return( 'unknown' ) ; @@ -16059,14 +18371,16 @@ sub loadavg sub loadavg_linux { - my $line = shift ; + my $line = shift @ARG ; - if ( ! $line ) { + if ( ! $line ) + { $line = firstline( '/proc/loadavg' ) or return ; } my ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) = split /\s/mxs, $line ; - if ( all_defined( $avg_1_min, $avg_5_min, $avg_15_min ) ) { + if ( all_defined( $avg_1_min, $avg_5_min, $avg_15_min ) ) + { $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min $current_runs\n" ) ; return ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) ; } @@ -16075,18 +18389,21 @@ sub loadavg_linux sub loadavg_freebsd { - my $file = shift ; + my $file = shift @ARG ; # Example of output of command "sysctl vm.loadavg": # vm.loadavg: { 0.15 0.08 0.08 } my $loadavg ; - if ( ! defined $file ) { + if ( ! defined $file ) + { eval { - $loadavg = `/sbin/sysctl vm.loadavg` ; + $loadavg = `LANG=C /sbin/sysctl vm.loadavg` ; #myprint( "LOADAVG FREEBSD: $loadavg\n" ) ; } ; if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } - }else{ + } + else + { $loadavg = firstline( $file ) or return ; } @@ -16098,15 +18415,16 @@ sub loadavg_freebsd sub loadavg_darwin { - my $line = shift ; + my $line = shift @ARG ; # Example of output of command "sysctl vm.loadavg": # vm.loadavg: { 0.15 0.08 0.08 } my $loadavg ; - if ( ! defined $line ) { + if ( ! defined $line ) + { eval { # $loadavg = `/usr/sbin/sysctl vm.loadavg` ; - $loadavg = `LANG= /usr/sbin/sysctl vm.loadavg` ; + $loadavg = `LANG=C /usr/sbin/sysctl vm.loadavg` ; #myprint( "LOADAVG DARWIN: $loadavg\n" ) ; } ; if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } @@ -16122,13 +18440,14 @@ sub loadavg_darwin sub loadavg_windows { - my $file = shift ; + my $file = shift @ARG ; # Example of output of command "wmic cpu get loadpercentage": # LoadPercentage # 12 my $loadavg ; - if ( ! defined $file ) { + if ( ! defined $file ) + { eval { #$loadavg = `CMD wmic cpu get loadpercentage` ; $loadavg = "LoadPercentage\n0\n" ; @@ -16152,69 +18471,94 @@ sub loadavg_windows -sub tests_load_and_delay +sub tests_load_and_delay { note( 'Entering tests_load_and_delay()' ) ; - is( undef, load_and_delay( ), 'load_and_delay: no args => undef ' ) ; - is( undef, load_and_delay( 1 ), 'load_and_delay: not 4 args => undef ' ) ; - is( undef, load_and_delay( 0, 1, 1, 1 ), 'load_and_delay: division per 0 => undef ' ) ; + is( undef, load_and_delay( ), 'load_and_delay: no args => undef ' ) ; + is( undef, load_and_delay( 1, 2 ), 'load_and_delay: not 3 args => undef ' ) ; + is( undef, load_and_delay( 2, 0, 1, 1, 1 ), 'load_and_delay: division per 0 => undef ' ) ; -# ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) +# ( $max_load_per_core, $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) - is( 0, load_and_delay( 1, 1, 1, 1 ), 'load_and_delay: one core, loads are all 1 => ok ' ) ; - is( 0, load_and_delay( 1, 1, 1, 1, 'lalala' ), 'load_and_delay: five arguments is ok' ) ; - is( 0, load_and_delay( 2, 2, 2, 2 ), 'load_and_delay: two core, loads are all 2 => ok ' ) ; - is( 0, load_and_delay( 2, 2, 4, 5 ), 'load_and_delay: two core, load1m is 2 => ok ' ) ; + # load max = 1 per cpu + is( 0, load_and_delay( 1, 1, 0 ), 'load_and_delay: max=1, one core, loads is 0 => ok ' ) ; + is( 0, load_and_delay( 1, 1, 0, 0, 0 ), 'load_and_delay: max=1, one core, loads are all 0 => ok ' ) ; + is( 0, load_and_delay( 3, 1, 1, 1, 1, 1 ), 'load_and_delay: six arguments => ok' ) ; + is( 0, load_and_delay( 1, 2, 1, 1, 1 ), 'load_and_delay: max=1, two core, loads are all 1 => ok ' ) ; + is( 0, load_and_delay( 1, 2, 1, 4, 5 ), 'load_and_delay: max=1, two core, load1m is 1 => ok ' ) ; + is( 0, load_and_delay( 1, 1, 0, 0, 0 ), 'load_and_delay: max=1, one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, 0, 0, 2 ), 'load_and_delay: max=1, one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, 0, 2, 0 ), 'load_and_delay: max=1, one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, 0, 2, 2 ), 'load_and_delay: max=1, one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, 0, 3, 3 ), 'load_and_delay: max=1, one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, 0, 4, 4 ), 'load_and_delay: max=1, one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, .2, 0, 0 ), 'load_and_delay: max=1, one core, load1m=.2 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, .2, 0, .2 ), 'load_and_delay: max=1, one core, load1m=.2 load5m=0 load15m=.2 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, .2, .2, 0 ), 'load_and_delay: max=1, one core, load1m=2 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 1, .2, .2, .2 ), 'load_and_delay: max=1, one core, load1m=.2 load5m=.2 load15m=.2 => 0 ' ) ; + is( 1, load_and_delay( 1, 1, 1, 0, 0 ), 'load_and_delay: max=1, one core, load1m=3 load5m=0 load15m=0 => 0 ' ) ; + is( 1, load_and_delay( 1, 1, 1, .9, .9 ), 'load_and_delay: max=1, one core, load1m=3 load5m=.9 load15m=.9 => 0 ' ) ; + is( 5, load_and_delay( 1, 1, 1, 1, .9 ), 'load_and_delay: max=1, one core, load1m=3 load5m=3 load15m=.9 => 5 ' ) ; + is( 15, load_and_delay( 1, 1, 1, 1, 1 ), 'load_and_delay: max=1, one core, load1m=3 load5m=3 load15m=3 => 15 ' ) ; + is( 0, load_and_delay( 1, 1, .9, .9, .9 ), 'load_and_delay: max=1, one core, load1m=.9 load5m=.9 load15m=.9 => 0 ' ) ; + + # load max = 3 per cpu + is( 0, load_and_delay( 3, 1, 1, 1, 1 ), 'load_and_delay: max=3, one core, loads are all 1 => ok ' ) ; + is( 0, load_and_delay( 3, 2, 2, 2, 2 ), 'load_and_delay: max=3, two core, loads are all 2 => ok ' ) ; + is( 0, load_and_delay( 3, 2, 2, 4, 5 ), 'load_and_delay: max=3, two core, load1m is 2 => ok ' ) ; + is( 0, load_and_delay( 3, 1, 0, 0, 0 ), 'load_and_delay: max=3, one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 0, 0, 2 ), 'load_and_delay: max=3, one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 0, 2, 0 ), 'load_and_delay: max=3, one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 0, 2, 2 ), 'load_and_delay: max=3, one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 0, 3, 3 ), 'load_and_delay: max=3, one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 0, 4, 4 ), 'load_and_delay: max=3, one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 2, 0, 0 ), 'load_and_delay: max=3, one core, load1m=2 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 2, 0, 2 ), 'load_and_delay: max=3, one core, load1m=2 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 2, 2, 0 ), 'load_and_delay: max=3, one core, load1m=2 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 2, 2, 2 ), 'load_and_delay: max=3, one core, load1m=2 load5m=2 load15m=2 => 0 ' ) ; + is( 1, load_and_delay( 3, 1, 3, 0, 0 ), 'load_and_delay: max=3, one core, load1m=3 load5m=0 load15m=0 => 0 ' ) ; + is( 1, load_and_delay( 3, 1, 3, 2.9, 2.9 ), 'load_and_delay: max=3, one core, load1m=3 load5m=2.9 load15m=2.9 => 0 ' ) ; + is( 5, load_and_delay( 3, 1, 3, 3, 2.9 ), 'load_and_delay: max=3, one core, load1m=3 load5m=3 load15m=2.9 => 0 ' ) ; + is( 15, load_and_delay( 3, 1, 3, 3, 3 ), 'load_and_delay: max=3, one core, load1m=3 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 3, 1, 2.9, 2.9, 2.9 ), 'load_and_delay: max=3, one core, load1m=2.9 load5m=2.9 load15m=2.9 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 0, 0 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 0, 2 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 2, 0 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 2, 2 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 3, 3 ), 'load_and_delay: one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 4, 4 ), 'load_and_delay: one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; - is( 0, load_and_delay( 1, 2, 0, 0 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 2, 0, 2 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=2 => 0 ' ) ; - is( 0, load_and_delay( 1, 2, 2, 0 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 2, 2, 2 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=2 => 0 ' ) ; - is( 0, load_and_delay( 1, 2.9, 2.9, 2.9 ), 'load_and_delay: one core, load1m=2.9 load5m=2.9 load15m=2.9 => 0 ' ) ; - - is( 0, load_and_delay( 1, 3, 0, 0 ), 'load_and_delay: one core, load1m=3 load5m=0 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 3, 2.9, 2.9 ), 'load_and_delay: one core, load1m=3 load5m=2.9 load15m=2.9 => 0 ' ) ; - is( 0, load_and_delay( 1, 3, 3, 2.9 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=2.9 => 0 ' ) ; - is( 0, load_and_delay( 1, 3, 3, 3 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=3 => 0 ' ) ; - - is( 1, load_and_delay( 1, 6, 0, 0 ), 'load_and_delay: one core, load1m=3 load5m=0 load15m=0 => 1 ' ) ; - is( 1, load_and_delay( 1, 6, 5.9, 5.9 ), 'load_and_delay: one core, load1m=3 load5m=2.9 load15m=2.9 => 1 ' ) ; - is( 5, load_and_delay( 1, 6, 6, 5.9 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=2.9 => 5 ' ) ; - is( 15, load_and_delay( 1, 6, 6, 6 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=3 => 15 ' ) ; - + is( 1, load_and_delay( 6, 1, 6, 0, 0 ), 'load_and_delay: max=6, one core, load1m=6 load5m=0 load15m=0 => 1 ' ) ; + is( 1, load_and_delay( 6, 1, 6, 5.9, 5.9 ), 'load_and_delay: max=6, one core, load1m=6 load5m=5.9 load15m=5.9 => 1 ' ) ; + is( 5, load_and_delay( 6, 1, 6, 6, 5.9 ), 'load_and_delay: max=6, one core, load1m=6 load5m=6 load15m=5.9 => 5 ' ) ; + is( 15, load_and_delay( 6, 1, 6, 6, 6 ), 'load_and_delay: max=6, one core, load1m=6 load5m=6 load15m=6 => 15 ' ) ; + is( 0, load_and_delay( 6, 1, 5.9, 5.9, 5.9 ), 'load_and_delay: max=6, one core, load1m=5.9 load5m=5.9 load15m=5.9 => 1 ' ) ; note( 'Leaving tests_load_and_delay()' ) ; return ; -} +} -sub load_and_delay +sub load_and_delay { - # Basically return 0 if load is not heavy, ie <= 1 per processor + # Basically return 0 if load is not heavy, ie load 1 min <= $max_load_per_core - # Not enough arguments - if ( 4 > scalar @ARG ) { return ; } + # 5 arguments at least (loadavg return 4 arguments like "0.19 0.16 0.17 3/444" ) + if ( 3 > scalar @ARG ) { return ; } - my ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) = @ARG ; + my ( $max_load_per_core, $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) = @ARG ; if ( 0 == $cpu_num ) { return ; } + # No avg_5_min nor avg_15_min on Windows + $avg_5_min ||= 0 ; + $avg_15_min ||= 0 ; + # Let divide by number of cores ( $avg_1_min, $avg_5_min, $avg_15_min ) = map { $_ / $cpu_num } ( $avg_1_min, $avg_5_min, $avg_15_min ) ; - # One of avg ok => ok, for now it is a OR - if ( $avg_1_min < 6 ) { return 0 ; } - if ( $avg_5_min < 6 ) { return 1 ; } # Retry in 1 minute - if ( $avg_15_min < 6 ) { return 5 ; } # Retry in 5 minutes - return 15 ; # Retry in 15 minutes -} + + # If one period 1m is ok => ok ( 0 ) else Nok ( return > 0, 1 or 5 or 15 ) + if ( $avg_1_min < $max_load_per_core ) { return 0 ; } + if ( $avg_5_min < $max_load_per_core ) { return 1 ; } # Message: retry in 1 minute + if ( $avg_15_min < $max_load_per_core ) { return 5 ; } # Message: retry in 5 minutes + return 15 ; # Message: retry in 15 minutes +} sub tests_cpu_time @@ -16225,7 +18569,7 @@ sub tests_cpu_time my $mysync = { } ; $mysync->{ debug } = 1 ; - ok( is_number( cpu_time( $mysync ) ), 'cpu_time: {} => a number' ) ; + ok( is_number( cpu_time( $mysync ) ), 'cpu_time: {} => a number' ) ; note( 'Leaving tests_cpu_time()' ) ; return ; @@ -16233,16 +18577,16 @@ sub tests_cpu_time sub cpu_time { - my $mysync = shift ; - + my $mysync = shift @ARG ; + my @cpu_times = times ; if ( ! @cpu_times ) { return ; } - + my $cpu_time = 0 ; # last element is the sum of all elements $cpu_time = ( map { $cpu_time += $_ } @cpu_times )[ -1 ] ; - my $cpu_time_round = mysprintf( '%.2f', $cpu_time ) ; - $mysync->{ debug } and myprint( join(' + ', @cpu_times), " = $cpu_time ~ $cpu_time_round\n" ) ; + my $cpu_time_rounded = mysprintf( '%.2f', $cpu_time ) ; + $mysync->{ debug } and myprint( join(' + ', @cpu_times), " = $cpu_time ~ $cpu_time_rounded\n" ) ; return $cpu_time ; } @@ -16266,7 +18610,7 @@ sub tests_cpu_percent sub cpu_percent { - my $mysync = shift ; + my $mysync = shift @ARG ; my $cpu_time = shift || 0 ; my $timediff = shift || 1 ; # no division by 0 @@ -16277,35 +18621,36 @@ sub cpu_percent my $cpu_percent = 0 ; $cpu_percent = mysprintf( '%.1f', 100 * $cpu_time / $timediff ) ; $mysync->{ debug } and myprint( "cpu_percent: $cpu_percent \n" ) ; - + return $cpu_percent ; - + } sub tests_cpu_percent_global { note( 'Entering tests_cpu_percent_global()' ) ; - is( '0.0', cpu_percent_global( ), 'cpu_percent_global: no args => 0' ) ; + is( '0.0', cpu_percent_global( ), 'tests_cpu_percent_global: no args => 0' ) ; my $mysync = { } ; $mysync->{ debug } = 1 ; - is( '0.0', cpu_percent_global( $mysync ), 'cpu_percent_global: {} => 0' ) ; - is( '0.0', cpu_percent_global( $mysync, 0 ), 'cpu_percent_global: {} 0 => 0' ) ; + is( '0.0', cpu_percent_global( $mysync ), 'tests_cpu_percent_global: {} => 0' ) ; + is( '0.0', cpu_percent_global( $mysync, 0 ), 'tests_cpu_percent_global: {} 0 => 0' ) ; SKIP: { if ( ! ( 'i005' eq hostname() ) ) { skip( 'cpu_percent_global on host != i005', 1 ) ; } - is( '25.0', cpu_percent_global( $mysync, 100 ), 'cpu_percent_global: {} 100 => 25 on host i005' ) ; + is( '25.0', cpu_percent_global( $mysync, 100 ), 'tests_cpu_percent_global: {} 100 => 25 on host i005' ) ; } ; - + SKIP: { if ( ! ( 'petite' eq hostname() ) ) { skip( 'cpu_percent_global on host != petite', 1 ) ; } - is( '50.0', cpu_percent_global( $mysync, 100 ), 'cpu_percent_global: {} 100 => 50 on host petite' ) ; + #is( '50.0', cpu_percent_global( $mysync, 100 ), 'tests_cpu_percent_global: {} 100 => 50 on host petite' ) ; + is( '100.0', cpu_percent_global( $mysync, 100, 1 ), 'tests_cpu_percent_global: {} 100 => 100 on host petite' ) ; } ; note( 'Leaving tests_cpu_percent_global()' ) ; @@ -16314,29 +18659,94 @@ sub tests_cpu_percent_global sub cpu_percent_global { - my $mysync = shift ; + my $mysync = shift @ARG ; my $cpu_percent = shift || 0 ; - - my $cpu_number = cpu_number( ) ; - + my $cpu_number_forced = shift @ARG ; + + my $cpu_number = cpu_number( $cpu_number_forced ) ; + my $cpu_percent_global ; $cpu_percent_global = mysprintf( '%.1f', $cpu_percent / $cpu_number ) ; $mysync->{ debug } and myprint( "cpu_percent_global: $cpu_percent_global \n" ) ; - + return( $cpu_percent_global ) ; } +sub tests_ram_memory_info +{ + note( 'Entering tests_ram_memory_info()' ) ; + + note( "ram_memory_info:", ram_memory_info( ) ) ; + note( "total_ram_memory_bytes_sys_meminfo:", total_ram_memory_bytes_sys_meminfo( ) ) ; + #note( "total_ram_memory_bytes_hw_memsize:", total_ram_memory_bytes_hw_memsize( ) ) ; + + ok( ram_memory_info( ), 'ram_memory_info: => some text' ) ; + + note( 'Leaving tests_ram_memory_info()' ) ; + return ; +} + + +sub total_ram_memory_bytes +{ + my $total_ram_memory_bytes ; + + # Found Sys::MemInfo::get( "totalmem" ) + # wrong on macOS 11.5.2 (20G95) + # good on Mac OS X 10.7.5 (11G63) + # ( commands "sw_vers" or "system_profiler SPSoftwareDataType" ) + + if ( 'darwin' eq $OSNAME ) + { + $total_ram_memory_bytes = total_ram_memory_bytes_hw_memsize( ) ; + } + else + { + $total_ram_memory_bytes = total_ram_memory_bytes_sys_meminfo( ) ; + } + + return $total_ram_memory_bytes ; +} + + + +sub total_ram_memory_bytes_hw_memsize +{ + my $total_ram_memory_bytes = `sysctl -n hw.memsize` ; + chomp $total_ram_memory_bytes ; + return $total_ram_memory_bytes ; +} + + +sub total_ram_memory_bytes_sys_meminfo +{ + my $total_ram_memory_bytes = Sys::MemInfo::get( "totalmem" ) ; + return $total_ram_memory_bytes ; +} + + + + sub ram_memory_info { + my $mysync = shift @ARG ; # In GigaBytes so division by 1024 * 1024 * 1024 # - return( - sprintf( "%.1f/%.1f free GiB of RAM", + + my $ram_memory_info = sprintf( + "%.1f/%.1f free GiB of RAM", Sys::MemInfo::get("freemem") / ( $KIBI ** 3 ), - Sys::MemInfo::get("totalmem") / ( $KIBI ** 3 ), - ) - ) ; + total_ram_memory_bytes( ) / ( $KIBI ** 3 ), + ) ; + + my $memory_consumption_all_pids_percent = memory_consumption_all_pids_percent( $mysync ) || 0 ; + if ( $memory_consumption_all_pids_percent ) + { + $ram_memory_info .= ", $memory_consumption_all_pids_percent% used by processes." + } + + return $ram_memory_info ; } @@ -16344,7 +18754,8 @@ sub ram_memory_info sub tests_memory_stress { note( 'Entering tests_memory_stress()' ) ; - + my $mysync = { } ; + sig_install( $mysync, 'catch_ignore', ( 'QUIT', 'TERM', 'INT' ) ) ; is( undef, memory_stress( ), 'memory_stress: => undef' ) ; note( 'Leaving tests_memory_stress()' ) ; @@ -16353,34 +18764,35 @@ sub tests_memory_stress sub memory_stress { - my $total_ram_in_MB = Sys::MemInfo::get("totalmem") / ( $KIBI * $KIBI ) ; + my $total_ram_in_MB = total_ram_memory_bytes( ) / ( $KIBI * $KIBI ) ; my $i = 1 ; - myprintf("Stress memory consumption before: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; - while ( $i < $total_ram_in_MB / 1.7 ) { $a .= "A" x 1000_000; $i++ } ; - myprintf("Stress memory consumption after: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; + myprintf("Stress memory consumption before: %.1f MiB of %.1f MiB my PID is $PROCESS_ID\n", memory_consumption_of_myself( ) / $KIBI / $KIBI, $total_ram_in_MB ) ; + while ( $i < $total_ram_in_MB / 1.7 ) + { + $a .= "A" x 1000_000; + myprint( "$i " ) ; + $i++ ; + } ; + myprintf( "\nStress memory consumption after: %.1f MiB of %.1f MiB my PID is $PROCESS_ID\n", memory_consumption_of_myself( ) / $KIBI / $KIBI, $total_ram_in_MB ) ; return ; } -sub tests_memory_consumption +sub tests_memory_consumption_of_myself { - note( 'Entering tests_memory_consumption()' ) ; + note( 'Entering tests_memory_consumption_of_myself()' ) ; - note( "memory_consumption: " . memory_consumption() . " bytes aka " . bytes_display_string_dec( memory_consumption() ) ) ; - like( memory_consumption( ), qr{\d+}xms,'memory_consumption no args') ; - like( memory_consumption( 1 ), qr{\d+}xms,'memory_consumption 1') ; - like( memory_consumption( $PROCESS_ID ), qr{\d+}xms,"memory_consumption_of_pids $PROCESS_ID") ; - - like( memory_consumption_ratio(), qr{\d+}xms, 'memory_consumption_ratio' ) ; - like( memory_consumption_ratio(1), qr{\d+}xms, 'memory_consumption_ratio 1' ) ; - like( memory_consumption_ratio(10), qr{\d+}xms, 'memory_consumption_ratio 10' ) ; + note( "memory_consumption_of_myself: " . memory_consumption_of_myself( ) . " bytes aka " . bytes_display_string_dec( memory_consumption_of_myself( ) ) ) ; + like( memory_consumption_of_myself( ), qr{\d+}xms,'tests_memory_consumption_of_myself no args') ; + like( memory_consumption_of_myself( 1 ), qr{\d+}xms,'tests_memory_consumption_of_myself 1') ; + like( memory_consumption_of_myself( $PROCESS_ID ), qr{\d+}xms,"tests_memory_consumption_of_myself $PROCESS_ID") ; - note( 'Leaving tests_memory_consumption()' ) ; + note( 'Leaving tests_memory_consumption_of_myself()' ) ; return ; } -sub memory_consumption +sub memory_consumption_of_myself { # memory consumed by imapsync until now in bytes return( ( memory_consumption_of_pids( ) )[0] ); @@ -16388,89 +18800,300 @@ sub memory_consumption sub debugmemory { - my $mysync = shift ; - if ( ! $mysync->{debugmemory} ) { return q{} ; } + my $mysync = shift @ARG ; + if ( ! $mysync->{ debugmemory } ) { return q{} ; } - my $precision = shift ; - return( mysprintf( "Memory consumption$precision: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ) ; + my $precision = shift @ARG ; + return( mysprintf( "Memory consumption$precision: %.1f MiB\n", memory_consumption_of_myself( ) / $KIBI / $KIBI ) ) ; } -sub memory_consumption_of_pids +sub memory_consumption_of_pids { - - my @pid = @_; + my @pid = @ARG ; + # No pid in ARG means find myself memory @pid = ( @pid ) ? @pid : ( $PROCESS_ID ) ; - $sync->{ debug } and myprint( "memory_consumption_of_pids PIDs: @pid\n" ) ; + #$sync->{ debug } and myprint( "memory_consumption_of_pids PIDs: @pid\n" ) ; my @val ; - if ( ( 'MSWin32' eq $OSNAME ) or ( 'cygwin' eq $OSNAME ) ) { - @val = memory_consumption_of_pids_win32( @pid ) ; - } - elsif ( 'darwin' eq $OSNAME ) + if ( ( 'MSWin32' eq $OSNAME ) or ( 'cygwin' eq $OSNAME ) ) { - @val = memory_consumption_of_pids_mac( @pid ) ; + @val = memory_consumption_of_pids_win32( @pid ) ; } else { - # Unix - my @ps = qx{ ps -o vsz -p @pid } ; - shift @ps ; # First line is column name "VSZ" - chomp @ps ; - # convert to octets - - @val = map { $_ * $KIBI } @ps ; + # Unix, Mac OS X included + @val = memory_consumption_of_pids_unix( @pid ) ; } return( @val ) ; -} +} -sub memory_consumption_of_pids_mac +sub memory_consumption_of_pids_unix { - my @pid = @_ ; + my @pid = @ARG ; # Use IPC::Open3 from perlcrit -3 # But it stalls on Darwin, I don't understand why! #my @ps = backtick( "ps -o rss -p @pid" ) ; #myprint( "ps: @ps" ) ; my @ps = qx{ ps -o rss -p @pid } ; - shift @ps ; # First line is column name "RSS" + shift @ps ; # First line is the column name "RSS" chomp @ps ; + # convert to octets my @val = map { $_ * $KIBI } @ps ; return( @val ) ; } -sub memory_consumption_of_pids_win32 + +sub tests_memory_consumption_of_all_pids { + note( 'Entering tests_memory_consumption_of_all_pids()' ) ; + + note( "memory_consumption_of_all_pids: " . memory_consumption_of_all_pids( ) . " bytes aka " . bytes_display_string_dec( memory_consumption_of_all_pids( ) ) ) ; + like( memory_consumption_of_all_pids( ), qr{\d+}xms, 'tests_memory_consumption_of_all_pids no args') ; + + note( 'Leaving tests_memory_consumption_of_all_pids()' ) ; + return ; +} + + +sub memory_consumption_of_all_pids +{ + my @all_pids = all_pids( ) ; + my @memory_consumption_of_all_pids = memory_consumption_of_pids( @all_pids ) ; + my $memory_consumption_of_all_pids = add( @memory_consumption_of_all_pids ) ; + return $memory_consumption_of_all_pids ; +} + + +sub tests_all_pids +{ + note( 'Entering tests_all_pids()' ) ; + + note( 'all_pids', join( ' ', all_pids( ) ) ) ; + ok( all_pids( ), 'tests_all_pids: no args => list of pids' ) ; + + + note( 'Leaving tests_all_pids()' ) ; + return ; +} + + +sub all_pids +{ + my @all_pids ; + if ( ( 'MSWin32' eq $OSNAME ) or ( 'cygwin' eq $OSNAME ) ) { + @all_pids = all_pids_win32( ) ; + } + else + { + # Unix + @all_pids = all_pids_unix( ) ; + } + return( @all_pids ) ; +} + +sub all_pids_unix +{ + my @ps = qx{ ps -e -o pid } ; + shift @ps ; # First line is the column name "PID" + chomp @ps ; + return @ps ; +} + + + +sub tests_memory_consumption_of_pids_win32 +{ + note( 'Entering tests_memory_consumption_of_pids_win32()' ) ; + + note( memory_consumption_of_pids_win32( $PROCESS_ID ) ) ; + #ok( memory_consumption_of_pids_win32( $PROCESS_ID ), 'tests_memory_consumption_of_pids_win32: no args => ' ) ; + + note( 'Leaving tests_memory_consumption_of_pids_win32()' ) ; + return ; +} + + +sub memory_consumption_of_pids_win32 +{ # Windows - my @PID = @_; + my @PID = @ARG; my %PID; # hash of pids as key values - map { $PID{$_}++ } @PID; + map { $PID{ $_ }++ } @PID ; # Does not work but should work reading the tasklist documentation #@ps = qx{ tasklist /FI "PID eq @PID" }; - my @ps = qx{ tasklist /NH /FO CSV } ; - #my @ps = backtick( 'tasklist /NH /FO CSV' ) ; + my @ps = qx{ tasklist /NH /FO TABLE } ; + #my @ps = backtick( 'tasklist /NH /FO TABLE' ) ; #myprint( "-" x $STD_CHAR_PER_LINE, "\n", @ps, "-" x $STD_CHAR_PER_LINE, "\n" ) ; - my @val; - foreach my $line (@ps) { - my($name, $pid, $mem) = (split ',', $line )[0,1,4]; + my @val ; + #myprint( @ps ) ; + foreach my $line ( @ps ) + { + my( $name, $pid, $mem ) = ( split / +/, $line )[ 0, 1, 4 ] ; next if (! $pid); - #myprint( "[$name][$pid][$mem]" ) ; - if ($PID{remove_qq($pid)}) { - #myprint( "MATCH !\n" ) ; + #myprint( "[$name][$pid][$mem]\n" ) ; + $pid = remove_qq( $pid ) ; + if ( $PID{ $pid } ) + { + #myprint( "MATCH $pid\n" ) ; chomp $mem ; - $mem = remove_qq($mem); - $mem = remove_Ko($mem); - $mem = remove_not_num($mem); - #myprint( "[$mem]\n" ) ; - push @val, $mem * $KIBI; + $mem = remove_qq( $mem ) ; + $mem = remove_not_num( $mem ) ; + if ( is_number( $mem ) ) + { + #myprint( "[$mem] ", $mem * $KIBI, "\n" ) ; + push @val, $mem * $KIBI ; + } } } - return(@val); + return( @val ) ; } + +sub all_pids_win32 +{ + my @ps = qx{ tasklist /NH /FO CSV } ; + my @pids ; + foreach my $line ( @ps ) + { + my( $name, $pid, $mem ) = ( split ',', $line )[ 0, 1, 4 ] ; + next if ( ! $pid ) ; + #myprint( "[$name][$pid][$mem]\n" ) ; + $pid = remove_qq( $pid ) ; + push @pids, $pid ; + } + return( @pids ) ; +} + + + +sub tests_memory_consumption_all_pids_percent +{ + note( 'Entering tests_memory_consumption_all_pids_percent()' ) ; + + note( memory_consumption_all_pids_percent( ) . " (%)" ) ; + like( memory_consumption_all_pids_percent( ), qr{^0|^(\d+\.\d+)$}xms, 'tests_memory_consumption_all_pids_percent: no args => like 12.34' ) ; + + ok( 0 <= memory_consumption_all_pids_percent( ), 'tests_memory_consumption_all_pids_percent: > 0' ) ; + ok( 100 >= memory_consumption_all_pids_percent( ), 'tests_memory_consumption_all_pids_percent: <= 100' ) ; + note( 'Leaving tests_memory_consumption_all_pids_percent()' ) ; + + return ; +} + + + +sub memory_consumption_all_pids_percent +{ + my $percent ; + + my $memory_consumption_of_all_pids = memory_consumption_of_all_pids( ) ; + my $total_memory_bytes = total_ram_memory_bytes( ) || return 0 ; + + $percent = sprintf( "%.2f", 100 * $memory_consumption_of_all_pids / $total_memory_bytes ) ; + return $percent ; +} + + + +sub tests_memory_consumption_all_pids_percent_Proc_ProcessTable +{ + note( 'Entering tests_memory_consumption_all_pids_percent_Proc_ProcessTable()' ) ; + require_ok( 'Proc::ProcessTable' ) ; + + note( memory_consumption_all_pids_percent_Proc_ProcessTable( ) . " (%)" ) ; + like( memory_consumption_all_pids_percent_Proc_ProcessTable( ), qr{^0|^(\d+\.\d+)$}xms, 'tests_memory_consumption_all_pids_percent_Proc_ProcessTable: no args => like 12.34' ) ; + + my $mysync = { } ; + + like( memory_consumption_all_pids_percent_Proc_ProcessTable( $mysync ), qr{^0|^(\d+\.\d+)$}xms, 'tests_memory_consumption_all_pids_percent_Proc_ProcessTable: { } => like 12.34' ) ; + note( 'Leaving tests_memory_consumption_all_pids_percent_Proc_ProcessTable()' ) ; + + return ; +} + +# Proc::ProcessTable pctmem does NOT work for Win32 Darwin +# Negative values and negative zeros for Darwin +# zeros for Win32 +# +sub memory_consumption_all_pids_percent_Proc_ProcessTable +{ + my $mysync = shift @ARG ; + + my $percent ; + if ( eval { require Proc::ProcessTable } ) + { + my $table_all_processes = Proc::ProcessTable->new( ) ; + + if ( pctmem_available( $table_all_processes ) ) + { + #pctmem is memory percentage of a process + foreach my $process ( @{ $table_all_processes->table( ) } ) + { + #myprint( "pctmem: ", $process->pctmem( ), "\n" ) ; + # + $percent += max( 0, $process->pctmem( ) ) ; + } + } + else + { + $percent = 0 ; + } + } + else + { + $percent = 0 ; + } + + $percent = sprintf( "%.2f", $percent ) ; + + return $percent ; +} + + +sub tests_pctmem_available +{ + note( 'Entering tests_pctmem_available()' ) ; + + is( undef, pctmem_available( ), 'pctmem_available: no args => undef' ) ; + + SKIP: { + skip( 'no Proc::ProcessTable', 1 ) if ( ! eval { require Proc::ProcessTable } ) ; + my $table_all_processes = Proc::ProcessTable->new( ) ; + like( pctmem_available( $table_all_processes ), qr{^(0|1)$}xms, 'pctmem_available: => 0 or 1' ) ; + } + + note( 'Leaving tests_pctmem_available()' ) ; + return ; +} + + +sub pctmem_available +{ + my $table_all_processes = shift @ARG ; + + if ( ! defined $table_all_processes ) { return ; } ; + + my @fields = $table_all_processes->fields( ) ; + + my %fields = map { defined( $_ ) ? ( $_ => 1 ) : ( ) } @fields ; + + if ( exists( $fields{ pctmem } ) ) + { + return 1 ; + } + else + { + return 0 ; + } + + return ; +} + sub tests_backtick { note( 'Entering tests_backtick()' ) ; @@ -16526,7 +19149,7 @@ sub tests_backtick sub backtick { - my $command = shift ; + my $command = shift @ARG ; if ( ! $command ) { return ; } @@ -16550,7 +19173,8 @@ sub backtick #myprint( @output ) ; if ( $output[0] =~ /\Qopen3: exec of $command failed\E/mxs ) { return ; } - if ( wantarray ) { + if ( wantarray ) + { return( @output ) ; } else { return( join( q{}, @output) ) ; @@ -16645,7 +19269,7 @@ sub search_dyn_lib_locale_MSWin32 sub remove_not_num { - my $string = shift ; + my $string = shift @ARG ; $string =~ tr/0-9//cd ; #myprint( "tr [$string]\n" ) ; return( $string ) ; @@ -16655,48 +19279,58 @@ sub tests_remove_not_num { note( 'Entering tests_remove_not_num()' ) ; - ok( '123' eq remove_not_num( 123 ), 'remove_not_num( 123 )' ) ; - ok( '123' eq remove_not_num( '123' ), q{remove_not_num( '123' )} ) ; - ok( '123' eq remove_not_num( '12 3' ), q{remove_not_num( '12 3' )} ) ; - ok( '123' eq remove_not_num( 'a 12 3 Ko' ), q{remove_not_num( 'a 12 3 Ko' )} ) ; + is( '123', remove_not_num( 123 ), 'remove_not_num( 123 )' ) ; + is( '123', remove_not_num( '123' ), q{remove_not_num( '123' )} ) ; + is( '123', remove_not_num( '12 3' ), q{remove_not_num( '12 3' )} ) ; + is( '123', remove_not_num( 'a 12 3 Ko' ), q{remove_not_num( 'a 12 3 Ko' )} ) ; + is( '123', remove_not_num( 'a 12 3 K' ), q{remove_not_num( 'a 12 3 K' )} ) ; + is( '123', remove_not_num( 'a 12,3 K' ), q{remove_not_num( 'a 12, 3 K' )} ) ; + is( '173892', remove_not_num( 'a 173,892 K' ), q{remove_not_num( 'a 173,892 K' )} ) ; note( 'Leaving tests_remove_not_num()' ) ; return ; } -sub remove_Ko + + + +sub tests_remove_qq { - my $string = shift; - if ($string =~ /^(.*)\sKo$/xo) { - return($1); + note( 'Entering tests_remove_qq()' ) ; + + is( undef, remove_qq( ), 'tests_remove_qq: no args => undef' ) ; + is( '', remove_qq( '' ), 'tests_remove_qq: empty => empty' ) ; + is( 'ABC', remove_qq( 'ABC' ), 'tests_remove_qq: ABC => ABC' ) ; + is( 'ABC', remove_qq( '"ABC"' ), 'tests_remove_qq: "ABC" => ABC' ) ; + is( '"ABC', remove_qq( '""ABC"' ), 'tests_remove_qq: ""ABC" => "ABC' ) ; + is( 'ABC"', remove_qq( '"ABC""' ), 'tests_remove_qq: "ABC"" => ABC"' ) ; + is( '"ABC"', remove_qq( '""ABC""' ), 'tests_remove_qq: ""ABC"" => "ABC"' ) ; + + note( 'Leaving tests_remove_qq()' ) ; + return ; +} + + +sub remove_qq +{ + my $string = shift ; + + if ( ! defined $string ) { return ; } + + #myprint( "$string\n" ) ; + + if ( $string =~ /^"(.*)"$/xo ) + { + return( $1 ) ; }else{ - return($string); + return( $string ) ; } -} - -sub remove_qq -{ - my $string = shift; - if ($string =~ /^"(.*)"$/xo) { - return($1); - }else{ - return($string); - } -} - -sub memory_consumption_ratio -{ - - my ($base) = @_; - $base ||= 1; - my $consu = memory_consumption(); - return($consu / $base); -} +} sub date_from_rcs { - my $d = shift ; + my $d = shift @ARG ; my %num2mon = qw( 01 Jan 02 Feb 03 Mar 04 Apr 05 May 06 Jun 07 Jul 08 Aug 09 Sep 10 Oct 11 Nov 12 Dec ) ; if ($d =~ m{(\d{4})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) { @@ -16732,7 +19366,7 @@ sub good_date # outgoing format: internal date format # 24-Aug-2010 16:00:00 +0200 - my $d = shift ; + my $d = shift @ARG ; return(q{}) if not defined $d; SWITCH: { @@ -16885,7 +19519,7 @@ sub tests_good_date ok('"18-Dec-2002 15:07:00 +0000"' eq good_date('Wednesday, December 18, 2002 03:07 PM'), 'good_date kbtoys.com orders'); ok('"16-Dec-2004 02:01:49 -0500"' eq good_date('Dec 16 2004 02:01:49 -0500'), 'good_date jr.com orders'); ok('"21-Jun-2001 11:11:11 +0000"' eq good_date('21-Jun-2001'), 'good_date register.com domain transfer'); - ok('"18-Nov-2012 18:34:38 +0100"' eq good_date('Sun, 18 Nov 2012 18:34:38 +0100'), 'good_date pop2imap bug (Westeuropäische Normalzeit)'); + ok('"18-Nov-2012 18:34:38 +0100"' eq good_date('Sun, 18 Nov 2012 18:34:38 +0100'), 'good_date pop2imap bug (Westeuropäische Normalzeit)'); ok('"19-Sep-2015 16:11:07 +0000"' eq good_date('Date: 2015/09/19 16:11:07 '), 'good_date from RCS date' ) ; note( 'Leaving tests_good_date()' ) ; @@ -16967,7 +19601,7 @@ sub tests_nb_messages_in_2_not_in_1 sub nb_messages_in_2_not_in_1 { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( not defined $mysync ) { return ; } $mysync->{ nb_messages_in_2_not_in_1 } = scalar( @@ -16981,7 +19615,7 @@ sub nb_messages_in_2_not_in_1 sub nb_messages_in_1_not_in_2 { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( not defined $mysync ) { return ; } $mysync->{ nb_messages_in_1_not_in_2 } = scalar( @@ -16996,7 +19630,7 @@ sub nb_messages_in_1_not_in_2 sub comment_on_final_diff_in_1_not_in_2 { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( not defined $mysync or $mysync->{ justfolders } @@ -17053,7 +19687,7 @@ sub comment_on_final_diff_in_1_not_in_2 sub comment_on_final_diff_in_2_not_in_1 { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( not defined $mysync or $mysync->{ justfolders } @@ -17076,6 +19710,16 @@ sub comment_on_final_diff_in_2_not_in_1 $nb_identified_h2_messages, " identified messages in host2 are on host1.\n" ) ; } + elsif ( $mysync->{ delete2 } ) + { + myprint( "The sync was not strict, there were at least ", + $mysync->{ nb_messages_in_2_not_in_1 }, + " among ", + $nb_identified_h2_messages, + " identified messages in host2 that were not on host1.", + " Run a sync again with --delete2 to be sure of a strict sync.\n" + ) ; + } else { myprint( "The sync is not strict, there are ", @@ -17083,8 +19727,8 @@ sub comment_on_final_diff_in_2_not_in_1 " among ", $nb_identified_h2_messages, " identified messages in host2 that are not on host1.", - " Use --delete2 and sync again to delete them and have a strict sync.\n" - ) ; + " Use --delete2 and sync again to delete them and have a strict sync.\n" + ) ; } return ; } @@ -17140,7 +19784,8 @@ sub match if ( ( ! defined $var ) or ( ! defined $regex ) ) { return ; } # normal cases - if ( eval { $var =~ qr{$regex} } ) { + if ( eval { $var =~ qr{$regex} } ) + { return $var ; }elsif ( $EVAL_ERROR ) { myprint( "Fatal regex $regex\n" ) ; @@ -17195,7 +19840,8 @@ sub notmatch if ( ( ! defined $var ) or ( ! defined $regex ) ) { return ; } # normal cases - if ( eval { $var !~ $regex } ) { + if ( eval { $var !~ $regex } ) + { return 1 ; }elsif ( $EVAL_ERROR ) { myprint( "Fatal regex $regex\n" ) ; @@ -17207,45 +19853,183 @@ sub notmatch } -sub delete_folders_in_2_not_in_1 -{ - foreach my $folder ( @h2_folders_not_in_1 ) { - if ( defined $delete2foldersonly and eval "\$folder !~ $delete2foldersonly" ) { - myprint( "Not deleting $folder because of --delete2foldersonly $delete2foldersonly\n" ) ; - next ; + +sub tests_subfolder2_with_delete2folders +{ + note( 'Entering tests_subfolder2_with_delete2folders()' ) ; + + is( undef, subfolder2_with_delete2folders( ), 'tests_subfolder2_with_delete2folders: no args => undef' ) ; + + my $mysync = { } ; + is( undef, subfolder2_with_delete2folders( $mysync ), 'tests_subfolder2_with_delete2folders: { } => undef' ) ; + + $mysync->{ delete2foldersonly } = '/QQQ/' ; + is( undef, subfolder2_with_delete2folders( $mysync ), 'tests_subfolder2_with_delete2folders: only --delete2foldersonly /QQQ/ => undef' ) ; + + $mysync->{ subfolder2 } = 'SUB' ; + is( undef, subfolder2_with_delete2folders( $mysync ), 'tests_subfolder2_with_delete2folders: --delete2foldersonly /QQQ/ --subfolder2 SUB => undef' ) ; + + $mysync->{ delete2folders } = 1 ; + is( '/QQQ/', subfolder2_with_delete2folders( $mysync ), 'tests_subfolder2_with_delete2folders: --delete2foldersonly /QQQ/ --subfolder2 SUB --delete2folders => /QQQ/' ) ; + + $mysync->{ delete2foldersonly } = undef ; + is( 'm{SUB}', subfolder2_with_delete2folders( $mysync ), 'tests_subfolder2_with_delete2folders: --subfolder2 --delete2folders => m{SUB}' ) ; + is( 'm{SUB}', $mysync->{ delete2foldersonly }, 'tests_subfolder2_with_delete2folders: --subfolder2 --delete2folders => --delete2foldersonly m{SUB}' ) ; + + + note( 'Leaving tests_subfolder2_with_delete2folders()' ) ; + return ; +} + + + +sub subfolder2_with_delete2folders +{ + my $mysync = shift ; + + if ( $mysync->{ subfolder2 } and $mysync->{ delete2folders } ) + { + myprint( "Detected --subfolder2 $mysync->{ subfolder2 } --delete2folders\n" ) ; + if ( ! $mysync->{ delete2foldersonly } ) + { + myprint( "Adding --delete2foldersonly " . "m{" . $mysync->{ subfolder2 } . "}\n" ) ; + $mysync->{ delete2foldersonly } = "m{" . $mysync->{ subfolder2 } . "}" ; } - if ( defined $delete2foldersbutnot and eval "\$folder =~ $delete2foldersbutnot" ) { - myprint( "Not deleting $folder because of --delete2foldersbutnot $delete2foldersbutnot\n" ) ; - next ; - } - my $res = $sync->{dry} ; # always success in dry mode! - $sync->{imap2}->unsubscribe( $folder ) if ( ! $sync->{dry} ) ; - $res = $sync->{imap2}->delete( $folder ) if ( ! $sync->{dry} ) ; - if ( $res ) { - myprint( "Deleted $folder", "$sync->{dry_message}", "\n" ) ; - }else{ - myprint( "Deleting $folder failed", "\n" ) ; + else + { + myprint( "Detected also a --delete2foldersonly $mysync->{ delete2foldersonly } so you are on your own about this value.\n" ) ; } } + else + { + return ; + } + + return $mysync->{ delete2foldersonly } ; +} + + + +sub tests_delete_folders_in_2_not_in_1 +{ + note( 'Entering tests_delete_folders_in_2_not_in_1()' ) ; + + + is( undef, delete_folders_in_2_not_in_1( ), q{tests_delete_folders_in_2_not_in_1: undef} ) ; + my $syncT ; + is( undef, delete_folders_in_2_not_in_1( $syncT ), q{tests_delete_folders_in_2_not_in_1: undef 2} ) ; + my $imapT ; + $syncT->{ imap1 } = $imapT ; + is( undef, delete_folders_in_2_not_in_1( $syncT ), q{tests_delete_folders_in_2_not_in_1: undef imap} ) ; + + require_ok( "Test::MockObject" ) ; + $imapT = Test::MockObject->new( ) ; + $syncT->{ imap2 } = $imapT ; + + $imapT->set_true( 'IsUnconnected' ) ; + is( undef, delete_folders_in_2_not_in_1( $syncT ), q{tests_delete_folders_in_2_not_in_1: Unconnected imap} ) ; + + # Now connected tests + $imapT->set_false( 'IsUnconnected' ) ; + $imapT->mock( 'LastError', sub { q{LastError mocked} } ) ; + + $syncT->{ delete2folders } = 0 ; + is( undef, delete_folders_in_2_not_in_1( $syncT, qw( INBOX DELME1 DELME2 ) ), q{tests_delete_folders_in_2_not_in_1: --delete2folders OFF} ) ; + + $syncT->{ delete2folders } = 1 ; + $imapT->set_true( 'unsubscribe' ) ; + $imapT->set_true( 'delete' ) ; + $syncT->{ dry_message } = '' ; + + is_deeply( [ qw( DELME1 DELME2 ) ], [ delete_folders_in_2_not_in_1( $syncT, qw( DELME1 DELME2 ) ) ], q{tests_delete_folders_in_2_not_in_1: DELME1 DELME2 --delete2folders ON } ) ; + is_deeply( [ qw( DELME1 DELME2 ) ], [ delete_folders_in_2_not_in_1( $syncT, qw( INBOX DELME1 DELME2 Inbox ) ) ], q{tests_delete_folders_in_2_not_in_1: skip INBOX Inbox } ) ; + + $syncT->{ delete2foldersbutnot } = 'm{NOTDELME2}' ; + is_deeply( [ qw( DELME1 ) ], [ delete_folders_in_2_not_in_1( $syncT, qw( DELME1 NOTDELME2 ) ) ], q{tests_delete_folders_in_2_not_in_1: DELME1 --delete2foldersbutnot NOTDELME2} ) ; + + $syncT->{ delete2foldersonly } = 'm{DELME}' ; + is_deeply( [ qw( DELME1 DELME2 YESDELMETOO ) ], [ delete_folders_in_2_not_in_1( $syncT, qw( DELME1 NOTME DELME2 YESDELMETOO ) ) ], q{tests_delete_folders_in_2_not_in_1: --delete2foldersonly DELME } ) ; + + + + + + note( 'Leaving tests_delete_folders_in_2_not_in_1()' ) ; return ; } + + +sub delete_folders_in_2_not_in_1 +{ + myprint( "Entering delete_folders_in_2_not_in_1()\n" ) ; + my ( $mysync, @h2_folders_to_delete ) = @ARG ; + + if ( ! $mysync->{ delete2folders } ) { return ; } ; + + + my @folders_deleted = () ; + + foreach my $folder ( @h2_folders_to_delete ) + { + # INBOX is RFC mandatory + if ( uc $folder eq 'INBOX' ) + { + myprint( "Host2: Not deleting $folder, since it is RFC mandatory to exist.\n" ) ; + next ; + } + + if ( defined( $mysync->{ delete2foldersonly } ) and eval "\$folder !~ $mysync->{ delete2foldersonly }" ) + { + myprint( "Not deleting $folder because of --delete2foldersonly $mysync->{ delete2foldersonly }\n" ) ; + next ; + } + + if ( defined( $mysync->{ delete2foldersbutnot } ) and eval "\$folder =~ $mysync->{ delete2foldersbutnot }" ) + { + myprint( "Not deleting $folder because of --delete2foldersbutnot $mysync->{ delete2foldersbutnot }\n" ) ; + next ; + } + + my $result ; + + if ( ! $mysync->{ dry } ) + { + $mysync->{ imap2 }->unsubscribe( $folder ) ; + $result = $mysync->{ imap2 }->delete( $folder ) ; + } + + if ( $result || $mysync->{ dry } ) # always success in dry mode. + { + myprint( "Deleted $folder", "$mysync->{ dry_message }", "\n" ) ; + $mysync->{ deleted_folders_host2 }++ ; + push @folders_deleted, $folder ; + + }else{ + myprint( "Deleting $folder failed", "\n" ) ; + $mysync->{ deleted_folders_host2_failures }++ ; + } + } + myprint( "Leaving delete_folders_in_2_not_in_1()\n" ) ; + return @folders_deleted ; +} + sub delete_folder { - my ( $mysync, $imap, $folder, $Side ) = @_ ; + my ( $mysync, $imap, $folder, $Side ) = @ARG ; if ( ! $mysync ) { return ; } if ( ! $imap ) { return ; } if ( ! $folder ) { return ; } $Side ||= 'HostX' ; - my $res = $mysync->{dry} ; # always success in dry mode! - if ( ! $mysync->{dry} ) { + my $res = $mysync->{ dry } ; # always success in dry mode! + if ( ! $mysync->{ dry } ) { $imap->unsubscribe( $folder ) ; $res = $imap->delete( $folder ) ; } if ( $res ) { - myprint( "$Side deleted $folder", $mysync->{dry_message}, "\n" ) ; + myprint( "$Side deleted $folder", $mysync->{ dry_message }, "\n" ) ; return 1 ; }else{ myprint( "$Side deleting $folder failed", "\n" ) ; @@ -17255,18 +20039,21 @@ sub delete_folder sub delete1emptyfolders { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync ) { return ; } # abort if no parameter - if ( ! $mysync->{delete1emptyfolders} ) { return ; } # abort if --delete1emptyfolders off - my $imap = $mysync->{imap1} ; + if ( ! $mysync->{ delete1emptyfolders } ) { return ; } # abort if --delete1emptyfolders off + + my $imap = $mysync->{ imap1 } ; if ( ! $imap ) { return ; } # abort if no imap if ( $imap->IsUnconnected( ) ) { return ; } # abort if disconnected my %folders_kept ; myprint( qq{Host1 deleting empty folders\n} ) ; - foreach my $folder ( reverse sort @{ $mysync->{h1_folders_wanted} } ) { + foreach my $folder ( reverse sort @{ $mysync->{ h1_folders_wanted } } ) + { my $parenthood = $imap->is_parent( $folder ) ; - if ( defined $parenthood and $parenthood ) { + if ( defined $parenthood and $parenthood ) + { myprint( "Host1: folder $folder has subfolders\n" ) ; $folders_kept{ $folder }++ ; next ; @@ -17274,19 +20061,24 @@ sub delete1emptyfolders my $nb_messages_select = examine_folder_and_count( $mysync, $imap, $folder, 'Host1' ) ; if ( ! defined $nb_messages_select ) { next ; } # Select failed => Neither continue nor keep this folder } my $nb_messages_search = scalar( @{ $imap->messages( ) } ) ; - if ( 0 != $nb_messages_select and 0 != $nb_messages_search ) { + if ( 0 != $nb_messages_select and 0 != $nb_messages_search ) + { myprint( "Host1: folder $folder has messages: $nb_messages_search (search) $nb_messages_select (select)\n" ) ; $folders_kept{ $folder }++ ; next ; } - if ( 0 != $nb_messages_select + $nb_messages_search ) { + if ( 0 != $nb_messages_select + $nb_messages_search ) + { myprint( "Host1: folder $folder odd messages count: $nb_messages_search (search) $nb_messages_select (select)\n" ) ; $folders_kept{ $folder }++ ; next ; } - # Here we must have 0 messages by messages() aka "SEARCH ALL" and also "EXAMINE" - if ( uc $folder eq 'INBOX' ) { - myprint( "Host1: Not deleting $folder\n" ) ; + # Now, here we must have 0 messages by messages() aka "SEARCH ALL" and also "EXAMINE" + + # INBOX is RFC mandatory + if ( uc $folder eq 'INBOX' ) + { + myprint( "Host1: Not deleting $folder, since it is RFC mandatory to exist.\n" ) ; $folders_kept{ $folder }++ ; next ; } @@ -17294,9 +20086,12 @@ sub delete1emptyfolders # can not delete a SELECTed or EXAMINEd folder so closing it # could changed be SELECT INBOX $imap->close( ) ; # close after examine does not expunge; anyway expunging an empty folder... - if ( delete_folder( $mysync, $imap, $folder, 'Host1' ) ) { + if ( delete_folder( $mysync, $imap, $folder, 'Host1' ) ) + { next ; # Deleted, good! - }else{ + } + else + { $folders_kept{ $folder }++ ; next ; # Not deleted, bad! } @@ -17304,7 +20099,7 @@ sub delete1emptyfolders remove_deleted_folders_from_wanted_list( $mysync, %folders_kept ) ; myprint( qq{Host1 ended deleting empty folders\n} ) ; return ; -} +} sub remove_deleted_folders_from_wanted_list { @@ -17324,7 +20119,7 @@ sub remove_deleted_folders_from_wanted_list sub examine_folder_and_count { - my ( $mysync, $imap, $folder, $Side ) = @_ ; + my ( $mysync, $imap, $folder, $Side ) = @ARG ; $Side ||= 'HostX' ; if ( ! examine_folder( $mysync, $imap, $folder, $Side ) ) { @@ -17344,12 +20139,12 @@ sub tests_delete1emptyfolders my $syncT ; is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef 2} ) ; my $imapT ; - $syncT->{imap1} = $imapT ; + $syncT->{ imap1 } = $imapT ; is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef imap} ) ; require_ok( "Test::MockObject" ) ; $imapT = Test::MockObject->new( ) ; - $syncT->{imap1} = $imapT ; + $syncT->{ imap1 } = $imapT ; $imapT->set_true( 'IsUnconnected' ) ; is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: Unconnected imap} ) ; @@ -17430,7 +20225,7 @@ sub tests_delete1emptyfolders $imapT->mock( 'History', sub { ( q{* 0 EXISTS} ) } ) ; $imapT->mock( 'messages', sub { [ ] } ) ; $imapT->set_true( qw{ delete close unsubscribe } ) ; - $syncT->{dry_message} = q{ (not really since in a mocked test)} ; + $syncT->{ dry_message } = q{ (not really since in a mocked test)} ; tests_delete1emptyfolders_unit( $syncT, [ qw{ INBOX DELME1 DELME2 } ], @@ -17446,9 +20241,9 @@ sub tests_delete1emptyfolders_unit { note( 'Entering tests_delete1emptyfolders_unit()' ) ; - my $syncT = shift ; - my $folders1wanted_init_ref = shift ; - my $folders1wanted_after_ref = shift ; + my $syncT = shift @ARG ; + my $folders1wanted_init_ref = shift @ARG ; + my $folders1wanted_after_ref = shift @ARG ; my $comment = shift || q{delete1emptyfolders:} ; my @folders1wanted_init = @{ $folders1wanted_init_ref } ; @@ -17466,7 +20261,7 @@ sub tests_delete1emptyfolders_unit sub extract_header { - my $string = shift ; + my $string = shift @ARG ; my ( $header ) = split /\n\n/x, $string ; if ( ! $header ) { return( q{} ) ; } @@ -17501,8 +20296,9 @@ EOM return ; } -sub decompose_header{ - my $string = shift ; +sub decompose_header +{ + my $string = shift @ARG ; # a hash, for a keyword header KEY value are list of strings [VAL1, VAL1_other, etc] # Think of multiple "Received:" header lines. @@ -17518,12 +20314,12 @@ sub decompose_header{ if ( $line =~ m/(^[^:]+):\s(.*)/xo ) { $key = $1 ; $val = $2 ; - $debugdev and myprint( "DDD KV [$key] [$val]\n" ) ; + #$debug and myprint( "DDD KV [$key] [$val]\n" ) ; push @{ $header->{ $key } }, $val ; # blanc and value => value from previous line continues }elsif( $line =~ m/^(\s+)(.*)/xo ) { $val = $2 ; - $debugdev and myprint( "DDD V [$val]\n" ) ; + #$debug and myprint( "DDD V [$val]\n" ) ; @{ $header->{ $key } }[ $LAST ] .= " $val" if $key ; # dirty line? }else{ @@ -17534,7 +20330,7 @@ sub decompose_header{ #myprint( Data::Dumper->Dump( [ $header ] ) ) ; return( $header ) ; -} +} sub tests_decompose_header{ @@ -17676,7 +20472,7 @@ sub epoch # outgoing format: epoch - my $d = shift ; + my $d = shift @ARG ; return(q{}) if not defined $d; my ( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m ) ; @@ -17749,7 +20545,7 @@ sub tests_max_line_length sub max_line_length { - my $string = shift ; + my $string = shift @ARG ; my $max = 0 ; while ( $string =~ m/([^\n]*\n?)/msxg ) { @@ -17777,15 +20573,14 @@ sub set_checknoabletosearch } -sub tests_setlogfile +sub tests_setlogfile { note( 'Entering tests_setlogfile()' ) ; my $mysync = {} ; - $mysync->{logdir} = 'vallogdir' ; - $mysync->{logfile} = 'vallogfile.txt' ; - is( 'vallogdir/vallogfile.txt', setlogfile( $mysync ), - 'setlogfile: logdir vallogdir, logfile vallogfile.txt, vallogdir/vallogfile.txt' ) ; + $mysync->{ logdir } = 'vallogdir' ; + is( 'vallogdir/vallogfile.txt', setlogfile( $mysync, 'vallogfile.txt' ), + 'setlogfile: logdir vallogdir, vallogfile.txt => vallogdir/vallogfile.txt' ) ; SKIP: { skip( 'Too hard to have a well known timezone on Windows', 9 ) if ( 'MSWin32' eq $OSNAME ) ; @@ -17796,8 +20591,8 @@ sub tests_setlogfile timestart => 2, } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000__.txt", setlogfile( $mysync ), - "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000__.txt" ) ; + is( '1970_01_01_00_00_02_000__.txt', setlogfile( $mysync ), + 'setlogfile: default is like 1970_01_01_00_00_02_000__.txt' ) ; $mysync = { timestart => 2, @@ -17806,29 +20601,27 @@ sub tests_setlogfile abort => 1, } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_abort.txt", setlogfile( $mysync ), - "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_abort.txt" ) ; + is( '1970_01_01_00_00_02_000_user1_user2_abort.txt', setlogfile( $mysync ), + 'setlogfile: default abort is like 1970_01_01_00_00_02_000_user1_user2_abort.txt' ) ; $mysync = { timestart => 2, user1 => 'user1', user2 => 'user2', - remote => 'zzz', } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote.txt", setlogfile( $mysync ), - "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote.txt" ) ; + is( '1970_01_01_00_00_02_000_user1_user2_remote.txt', setlogfile( $mysync, undef, '_remote' ), + 'setlogfile: default with _remote is like 1970_01_01_00_00_02_000_user1_user2_remote.txt' ) ; $mysync = { timestart => 2, user1 => 'user1', user2 => 'user2', - remote => 'zzz', abort => 1, } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt", setlogfile( $mysync ), - "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt" ) ; + is( '1970_01_01_00_00_02_000_user1_user2_remote_abort.txt', setlogfile( $mysync, undef, '_remote' ), + 'setlogfile: default abort with _remote is like 1970_01_01_00_00_02_000_user1_user2_remote_abort.txt' ) ; $mysync = { @@ -17837,13 +20630,13 @@ sub tests_setlogfile user2 => 'user2', } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt", setlogfile( $mysync ), - "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt" ) ; + is( '1970_01_01_00_00_02_000_user1_user2.txt', setlogfile( $mysync ), + 'setlogfile: default is like 1970_01_01_00_00_02_000_user1_user2.txt' ) ; $mysync->{logdir} = undef ; $mysync->{logfile} = undef ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt", setlogfile( $mysync ), - "setlogfile: logdir undef, $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt" ) ; + is( '1970_01_01_00_00_02_000_user1_user2.txt', setlogfile( $mysync ), + 'setlogfile: logdir undef, 1970_01_01_00_00_02_000_user1_user2.txt' ) ; $mysync->{logdir} = q{} ; $mysync->{logfile} = undef ; @@ -17860,43 +20653,117 @@ sub tests_setlogfile user2 => 'u/ser2a*|?:"<>b', } ; - is( "$DEFAULT_LOGDIR/1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt", setlogfile( $mysync ), - "setlogfile: logdir undef, $DEFAULT_LOGDIR/1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt" ) ; - - + is( '1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt', setlogfile( $mysync ), + 'setlogfile: logdir undef, 1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt' ) ; } ; note( 'Leaving tests_setlogfile()' ) ; return ; -} +} -sub setlogfile + +sub setlogfile { - my( $mysync ) = shift ; + my $mysync = shift @ARG ; + my $given = shift @ARG ; + my $supplement = shift @ARG || '' ; # When aborting another process the log file name finishes with "_abort.txt" my $abort_suffix = ( $mysync->{ abort } ) ? '_abort' : q{} ; + + my $suffix = logfilesuffix( $mysync, $supplement . $abort_suffix ) ; - # When acting as a proxy the log file name finishes with "_remote.txt" - # proxy mode is not done in imapsync, it is done by proximapsync - my $remote_suffix = ( $mysync->{ remote } ) ? '_remote' : q{} ; + my $logdir = $mysync->{ logdir } || '' ; - my $suffix = ( - filter_forbidden_characters( slash_to_underscore( $mysync->{ user1 } ) ) || q{} ) - . '_' - . ( filter_forbidden_characters( slash_to_underscore( $mysync->{ user2 } ) ) || q{} ) - . $remote_suffix . $abort_suffix ; + my $logfile ; + if ( defined $given ) + { + if ( $logdir ) + { + $logfile = "$logdir/$given" ; + } + else + { + $logfile = "$given" ; + } + } + else + { + $logfile = logfile( $mysync->{ timestart }, $suffix, $logdir ) ; + } - $mysync->{ logdir } = defined $mysync->{ logdir } ? $mysync->{ logdir } : $DEFAULT_LOGDIR ; + return( $logfile ) ; +} - $mysync->{ logfile } = defined $mysync->{ logfile } - ? "$mysync->{ logdir }/$mysync->{ logfile }" - : logfile( $mysync->{ timestart }, $suffix, $mysync->{ logdir } ) ; - return( $mysync->{ logfile } ) ; +sub tests_logfilesuffix +{ + note( 'Entering tests_logfilesuffix()' ) ; + + is( '_', logfilesuffix( ), 'tests_logfilesuffix: no args => _' ) ; + my $mysync = { } ; + is( '_', logfilesuffix( $mysync ), 'tests_logfilesuffix: undef => _' ) ; + $mysync->{ user1 } = 'valuser1' ; + $mysync->{ user2 } = 'valuser2' ; + is( 'valuser1_valuser2', logfilesuffix( $mysync ), 'tests_logfilesuffix: valuser1 valuser2 => valuser1_valuser2' ) ; + is( 'valuser1_valuser2_suppl', logfilesuffix( $mysync, '_suppl' ), 'tests_logfilesuffix: valuser1 valuser2 _suppl => valuser1_valuser2_suppl' ) ; + + note( 'Leaving tests_logfilesuffix()' ) ; + return ; } +sub logfilesuffix +{ + my $mysync = shift @ARG ; + my $supplement = shift @ARG || '' ; + + my $suffix = ( + filter_forbidden_characters_cgi_context( slash_to_underscore( $mysync->{ user1 } ) ) || q{} ) + . '_' + . ( filter_forbidden_characters_cgi_context( slash_to_underscore( $mysync->{ user2 } ) ) || q{} ) + . $supplement ; + + return $suffix ; +} + + + +sub tests_setlogdir +{ + note( 'Entering tests_setlogdir()' ) ; + + is( $DEFAULT_LOGDIR, setlogdir( ), "setlogdir: no args => $DEFAULT_LOGDIR" ) ; + + my $mysync = { } ; + is( $DEFAULT_LOGDIR, setlogdir( $mysync ), "setlogdir: no args => $DEFAULT_LOGDIR" ) ; + + $mysync->{ logdir } = '' ; + is( '', setlogdir( $mysync ), "setlogdir: logdir empty string => empty string" ) ; + is( '', $mysync->{ logdir }, "setlogdir: logdir empty string unchanged" ) ; + + $mysync->{ logdir } = 'vallogdir' ; + is( 'vallogdir', setlogdir( $mysync ), "setlogdir: logdir vallogdir => vallogdir" ) ; + is( 'vallogdir', $mysync->{ logdir }, "setlogdir: logdir vallogdir unchanged" ) ; + # Does a second call hurt? + is( 'vallogdir', setlogdir( $mysync ), "setlogdir: logdir vallogdir => vallogdir" ) ; + is( 'vallogdir', $mysync->{ logdir }, "setlogdir: logdir vallogdir unchanged" ) ; + + note( 'Leaving tests_setlogdir()' ) ; + return ; +} + + +sub setlogdir +{ + my $mysync = shift @ARG ; + + my $logdir = defined $mysync->{ logdir } ? $mysync->{ logdir } : $DEFAULT_LOGDIR ; + + return $logdir ; +} + + sub tests_logfile { note( 'Entering tests_logfile()' ) ; @@ -17908,14 +20775,14 @@ sub tests_logfile local $ENV{TZ} = 'GMT' ; { POSIX::tzset unless ('MSWin32' eq $OSNAME) ; - is( '1970_01_01_00_00_00_000.txt', logfile( ), 'logfile: no args => 1970_01_01_00_00_00.txt' ) ; - is( '1970_01_01_00_00_00_000.txt', logfile( 0 ), 'logfile: 0 => 1970_01_01_00_00_00.txt' ) ; - is( '1970_01_01_00_01_01_000.txt', logfile( 61 ), 'logfile: 0 => 1970_01_01_00_01_01.txt' ) ; - is( '1970_01_01_00_01_01_234.txt', logfile( 61.234 ), 'logfile: 0 => 1970_01_01_00_01_01.txt' ) ; - is( '2010_08_24_14_00_00_000.txt', logfile( 1_282_658_400 ), 'logfile: 1_282_658_400 => 2010_08_24_14_00_00.txt' ) ; - is( '2010_08_24_14_01_01_000.txt', logfile( 1_282_658_461 ), 'logfile: 1_282_658_461 => 2010_08_24_14_01_01.txt' ) ; - is( '2010_08_24_14_01_01_000_poupinette.txt', logfile( 1_282_658_461, 'poupinette' ), 'logfile: 1_282_658_461 poupinette => 2010_08_24_14_01_01_poupinette.txt' ) ; - is( '2010_08_24_14_01_01_000_removeblanks.txt', logfile( 1_282_658_461, ' remove blanks ' ), 'logfile: 1_282_658_461 remove blanks => 2010_08_24_14_01_01_000_removeblanks' ) ; + is( '1970_01_01_00_00_00_000.txt', logfile( ), 'logfile: no args => 1970_01_01_00_00_00_000.txt' ) ; + is( '1970_01_01_00_00_00_000.txt', logfile( 0 ), 'logfile: 0 => 1970_01_01_00_00_00_000.txt' ) ; + is( '1970_01_01_00_01_01_000.txt', logfile( 61 ), 'logfile: 0 => 1970_01_01_00_01_01_000.txt' ) ; + is( '1970_01_01_00_01_01_234.txt', logfile( 61.234 ), 'logfile: 0 => 1970_01_01_00_01_01_234.txt' ) ; + is( '2010_08_24_14_00_00_000.txt', logfile( 1_282_658_400 ), 'logfile: 1_282_658_400 => 2010_08_24_14_00_00_000.txt' ) ; + is( '2010_08_24_14_01_01_000.txt', logfile( 1_282_658_461 ), 'logfile: 1_282_658_461 => 2010_08_24_14_01_01_000.txt' ) ; + is( '2010_08_24_14_01_01_000_poupinette.txt', logfile( 1_282_658_461, 'poupinette' ), 'logfile: 1_282_658_461 poupinette => 2010_08_24_14_01_01_000_poupinette.txt' ) ; + is( '2010_08_24_14_01_01_000_removeblanks.txt', logfile( 1_282_658_461, ' remove blanks ' ), 'logfile: 1_282_658_461 remove blanks => 2010_08_24_14_01_01_000_removeblanks.txt' ) ; is( '2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup' ), 'logfile: 1_282_658_461.2347 poup => 2010_08_24_14_01_01_234_poup.txt' ) ; @@ -17923,8 +20790,6 @@ sub tests_logfile is( 'dirdir/2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup', 'dirdir' ), 'logfile: 1_282_658_461.2347 poup dirdir => dirdir/2010_08_24_14_01_01_234_poup.txt' ) ; - - } POSIX::tzset unless ('MSWin32' eq $OSNAME) ; } ; @@ -17934,9 +20799,9 @@ sub tests_logfile } -sub logfile +sub logfile { - my ( $time, $suffix, $dir ) = @_ ; + my ( $time, $suffix, $dir ) = @ARG ; $time ||= 0 ; $suffix ||= q{} ; @@ -17945,47 +20810,148 @@ sub logfile $dir ||= q{} ; my $sep_dir = ( $dir ) ? '/' : q{} ; - my $date_str = POSIX::strftime( '%Y_%m_%d_%H_%M_%S', localtime $time ) ; - # Because of ab tests or web accesses, more than one sync withing one second is possible - # so we add also milliseconds - $date_str .= sprintf "_%03d", ($time - int( $time ) ) * 1000 ; # without rounding + my $date_str = year_month_day_hour_min_sec_ms( $time ) ; + my $logfile = "${dir}${sep_dir}${date_str}${sep_suffix}${suffix}.txt" ; return( $logfile ) ; } -sub tests_localtimez -{ - note( 'Entering tests_localtimez()' ) ; - SKIP: { - # Too hard to have a well known timezone on Windows - skip( 'Too hard to have a well known timezone on Windows', 1 ) if ( 'MSWin32' eq $OSNAME ) ; - local $ENV{TZ} = 'GMT' ; - like( localtimez( 0 ), qr'1970-01-01 00:00:00 \+0000 (GMT|UTC)', 'localtimez: 0 => match 1970-01-01 00:00:00 +0000 GMT' ) ; +sub tests_year_month_day_hour_min_sec_ms +{ + note( 'Entering tests_date_year_month_day_hour_min_sec_ms()' ) ; + + if ( 'MSWin32' eq $OSNAME ) + { + # Can not use $ENV{TZ} nor POSIX::tzset + like( year_month_day_hour_min_sec_ms( ), qr'1970_01_01_\d\d_00_00_000', 'year_month_day_hour_min_sec_ms: no args => match 1970_01_01_\d\d_00_00_000' ) ; + like( year_month_day_hour_min_sec_ms( 0 ), qr'1970_01_01_\d\d_00_00_000', 'year_month_day_hour_min_sec_ms: 0 => match 1970_01_01_\d\d_00_00_000' ) ; + like( year_month_day_hour_min_sec_ms( 1671706800.123 ), qr'2022_12_22_\d\d_00_00_122', 'year_month_day_hour_min_sec_ms: 123456789.123 => match 2022_12_22_\d\d_00_00_122' ) ; + + like( year_month_day_hour_min_sec_ms( -1 ), qr'19\d\d_\d\d_\d\d_\d\d_59_59_000', 'year_month_day_hour_min_sec_ms: -1 => 19\d\d_\d\d_\d\d_\d\d_59_59_000' ) ; + like( year_month_day_hour_min_sec_ms( -0.246 ), qr'19\d\d_\d\d_\d\d_\d\d_59_59_754', 'year_month_day_hour_min_sec_ms: -1 => 19\d\d_\d\d_\d\d_\d\d_59_59_754' ) ; + + like( year_month_day_hour_min_sec_ms( -32360400.135 ), qr'1968_12_22_\d\d_59_59_864', 'year_month_day_hour_min_sec_ms: -1 => 1968_12_22_\d\d_59_59_864' ) ; } + else + { + local $ENV{TZ} = 'GMT' ; + { + POSIX::tzset unless ('MSWin32' eq $OSNAME) ; + is( '1970_01_01_00_00_00_000', year_month_day_hour_min_sec_ms( ), + 'year_month_day_hour_min_sec_ms: no args => 1970_01_01_00_00_00_000 GMT' ) ; + is( '1970_01_01_00_00_00_000', year_month_day_hour_min_sec_ms( 0 ), + 'year_month_day_hour_min_sec_ms: 0 => 1970_01_01_00_00_00_000 GMT' ) ; + is( '1973_11_29_21_33_09_122', year_month_day_hour_min_sec_ms( 123456789.123 ), + 'year_month_day_hour_min_sec_ms: 123456789.123 => 1973_11_29_21_33_09_122 GMT' ) ; + is( '1969_12_31_23_59_59_000', year_month_day_hour_min_sec_ms( -1 ), + 'year_month_day_hour_min_sec_ms: -1 => 1969_12_31_23_59_59_000 GMT' ) ; + is( '1969_12_31_23_59_59_754', year_month_day_hour_min_sec_ms( -0.246 ), + 'year_month_day_hour_min_sec_ms: -0.246 => 1969_12_31_23_59_59_754 GMT' ) ; + is( '1966_02_02_02_26_50_864', year_month_day_hour_min_sec_ms( -123456789.135 ), + 'year_month_day_hour_min_sec_ms: -123456789.135 => 1966_02_02_02_26_50_864 GMT' ) ; + } + POSIX::tzset unless ('MSWin32' eq $OSNAME) ; + } + note( 'Leaving tests_year_month_day_hour_min_sec_ms()' ) ; + return ; +} - is( localtimez( ), localtimez( time ), 'localtimez: undef => equals currrent' ) ; - note( 'Leaving tests_localtimez()' ) ; +sub year_month_day_hour_min_sec_ms +{ + my $time = shift @ARG || 0 ; + + my $date_str = POSIX::strftime( '%Y_%m_%d_%H_%M_%S', localtime $time ) ; + # Because of ab tests or web accesses, more than one sync withing one second is possible + # so we add also milliseconds + $date_str .= sprintf "_%03d", fractional_of_floor( $time ) * 1000 ; + + return $date_str ; +} + + + +sub tests_fractional_of_floor +{ + note( 'Entering tests_fractional_of_floor()' ) ; + + is( 0, fractional_of_floor( ), 'fractional_of_floor: no args => 0' ) ; + is( 0, fractional_of_floor( 0 ), 'fractional_of_floor: 0 => 0' ) ; + is( 0, fractional_of_floor( '0' ), 'fractional_of_floor: 0 => 0' ) ; + is( 0, fractional_of_floor( 1 ), 'fractional_of_floor: 1 => 0' ) ; + is( 0, fractional_of_floor( '1' ), 'fractional_of_floor: 1 => 0' ) ; + is( 0, fractional_of_floor( -1 ), 'fractional_of_floor: -1 => 0' ) ; + is( 0, fractional_of_floor( '-1' ), 'fractional_of_floor: -1 => 0' ) ; + is( 0.234, fractional_of_floor( 1.234 ), 'fractional_of_floor: 1.234 => 0.234' ) ; + is( 0.234, fractional_of_floor( '1.234' ), 'fractional_of_floor: 1.234 => 0.234' ) ; + is( 0.766, fractional_of_floor( -1.234 ), 'fractional_of_floor: -1.234 => 0.766' ) ; + is( 0.766, fractional_of_floor( '-1.234' ), 'fractional_of_floor: -1.234 => 0.766' ) ; + is( 0.234, fractional_of_floor( 10.234 ), 'fractional_of_floor: 10.234 => 0.234' ) ; + is( 0.766, fractional_of_floor( -10.234 ), 'fractional_of_floor: -10.234 => 0.766' ) ; + + note( 'Leaving tests_fractional_of_floor()' ) ; return ; } -sub localtimez + +sub fractional_of_floor { - my $time = shift ; - - $time = defined( $time ) ? $time : time ; - - my $datetimestr = POSIX::strftime( '%A %e %B %Y-%m-%d %H:%M:%S %z %Z', localtime( $time ) ) ; - - #myprint( "$datetimestr\n" ) ; - return $datetimestr ; + my $float = shift @ARG || 0 ; + + if ( $float - int( $float ) >= 0 ) + { + return( $float - int( $float ) ) ; + } + else + { + return( 1 - ( int( $float ) - $float ) ) ; + } } +sub tests_localtimez +{ + note( 'Entering tests_localtimez()' ) ; + note( "localtimez: " . localtimez( ) ) ; + if ( 'MSWin32' eq $OSNAME ) + { + like( localtimez( 0 ), qr'1970-01-01 \d\d:\d\d:\d\d', 'localtimez: 0 => match 1970-01-01 \d\d:\d\d:\d\d' ) ; + } + else + { + local $ENV{TZ} = 'GMT' ; + like( localtimez( 0 ), qr'1970-01-01 00:00:00 \+0000 (GMT|UTC)', 'localtimez: 0 => match 1970-01-01 00:00:00 +0000 GMT or UTC' ) ; + } + is( localtimez( ), localtimez( time ), 'localtimez: undef => equals currrent' ) ; + note( 'Leaving tests_localtimez()' ) ; + return ; +} + +sub localtimez +{ + my $time = shift @ARG ; + + $time = defined( $time ) ? $time : time ; + + my $datetimestr ; + + if ( 'MSWin32' eq $OSNAME ) + { + $datetimestr = POSIX::strftime( '%A %d %B %Y-%m-%d %H:%M:%S %z', localtime( $time ) ) ; + } + else + { + $datetimestr = POSIX::strftime( '%A %d %B %Y-%m-%d %H:%M:%S %z %Z', localtime( $time ) ) ; + } + #myprint( "$datetimestr\n" ) ; + return $datetimestr ; +} + + sub tests_slash_to_underscore { @@ -18000,7 +20966,7 @@ sub tests_slash_to_underscore sub slash_to_underscore { - my $string = shift ; + my $string = shift @ARG ; if ( ! defined $string ) { return ; } @@ -18035,155 +21001,156 @@ sub tests_million_folders_baby_2 -sub tests_always_fail -{ - note( 'Entering tests_always_fail()' ) ; - - is( 0, 1, 'always_fail: 0 is 1' ) ; - - note( 'Leaving tests_always_fail()' ) ; - return ; -} - - -sub tests_logfileprepa -{ +sub tests_logfileprepa +{ note( 'Entering tests_logfileprepa()' ) ; is( undef, logfileprepa( ), 'logfileprepa: no args => undef' ) ; + + my $mysync = {} ; + is( undef, logfileprepa( $mysync ), 'logfileprepa: no logfile => undef' ) ; + my $logfile = 'W/tmp/tests/tests_logfileprepa.txt' ; - is( 1, logfileprepa( $logfile ), 'logfileprepa: W/tmp/tests/tests_logfileprepa.txt => 1' ) ; + is( 1, logfileprepa( $mysync, $logfile ), 'logfileprepa: W/tmp/tests/tests_logfileprepa.txt => 1' ) ; note( 'Leaving tests_logfileprepa()' ) ; return ; -} +} sub logfileprepa { - my $logfile = shift ; + my $mysync = shift @ARG ; + my $logfile = shift @ARG ; - if ( ! defined( $logfile ) ) + if ( ! defined( $mysync ) ) { return ; - }else + } + elsif ( ! defined( $logfile ) ) + { + return ; + } + else { - #myprint( "[$logfile]\n" ) ; my $dirname = dirname( $logfile ) ; - do_valid_directory( $dirname ) || return( 0 ) ; - return( 1 ) ; + return( make_valid_directory( $mysync, $dirname ) ) ; } } -sub tests_teelaunch -{ +sub tests_teelaunch +{ note( 'Entering tests_teelaunch()' ) ; - is( undef, teelaunch( ), 'teelaunch: no args => undef' ) ; + is( undef, teelaunch( ), 'tests_teelaunch: no args => undef' ) ; my $mysync = {} ; - is( undef, teelaunch( $mysync ), 'teelaunch: arg empty {} => undef' ) ; - $mysync->{logfile} = q{} ; - is( undef, teelaunch( $mysync ), 'teelaunch: logfile empty string => undef' ) ; + is( undef, teelaunch( $mysync ), 'tests_teelaunch: arg empty {} => undef' ) ; + is( undef, teelaunch( $mysync, '' ), 'tests_teelaunch: empty string => undef' ) ; # First time, learning IO::Tee intrasics - $mysync->{logfile} = 'W/tmp/tests/tests_teelaunch.txt' ; - isa_ok( my $tee = teelaunch( $mysync ), 'IO::Tee' , 'teelaunch: logfile W/tmp/tests/tests_teelaunch.txt' ) ; - is( 1, print( $tee "Hi!\n" ), 'teelaunch: write Hi!') ; - is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\n' ) ; - is( 1, print( $tee "Hoo\n" ), 'teelaunch: write Hoo') ; - is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\nHoo\n' ) ; + my $tee = teelaunch( $mysync, 'W/tmp/tests/tests_teelaunch.txt' ) ; + isa_ok( $tee, 'IO::Tee', 'tests_teelaunch: logfile W/tmp/tests/tests_teelaunch.txt' ) ; + is( 1, print( $tee "Hi!\n" ), 'tests_teelaunch: write Hi!') ; + is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\n' ) ; + is( 1, print( $tee "Hoo\n" ), 'tests_teelaunch: write Hoo') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\nHoo\n' ) ; - # closing so tee won't be happy - close $mysync->{logfile_handle} ; - is( undef, print( $tee "Argh1\n" ), 'teelaunch: write Argh1') ; - is( undef, print( $tee "Argh2\n" ), 'teelaunch: write Argh2') ; + # closing file handle so tee won't be happy + ($tee->handles)[0]->close ; + is( undef, print( $tee "Argh1\n" ), 'tests_teelaunch: write Argh1') ; + is( undef, print( $tee "Argh2\n" ), 'tests_teelaunch: write Argh2') ; # write not done - is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is still Hi!\nHoo\n' ) ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch.txt is still Hi!\nHoo\n' ) ; print join( ' ', $tee->handles ), "\n"; - is( 2, scalar $tee->handles, 'teelaunch: 2 handles') ; + is( 2, scalar $tee->handles, 'tests_teelaunch: 2 handles') ; shift @{*{$tee}}; print join(' ', $tee->handles), "\n" ; - is( 1, scalar $tee->handles, 'teelaunch: 1 handle') ; - is( 1, print( $tee "Argh3\n" ), 'teelaunch: write Argh3 yeah') ; + is( 1, scalar $tee->handles, 'tests_teelaunch: 1 handle') ; + is( 1, print( $tee "Argh3\n" ), 'tests_teelaunch: write Argh3 yeah') ; shift @{*{$tee}}; # will not print anything now - is( 0, scalar $tee->handles, 'teelaunch: 0 handle') ; - is( 1, print( $tee "Argh 4\n" ), 'teelaunch: write Argh4 no') ; + is( 0, scalar $tee->handles, 'tests_teelaunch: 0 handle') ; + is( 1, print( $tee "Argh 4\n" ), 'tests_teelaunch: write Argh4 no') ; # Second time, lesson learnt IO::Tee - $mysync->{logfile} = 'W/tmp/tests/tests_teelaunch2.txt' ; - isa_ok( $tee = teelaunch( $mysync ), 'IO::Tee' , 'teelaunch: logfile W/tmp/tests/tests_teelaunch2.txt' ) ; - is( 1, print( $tee "Hi!\n" ), 'teelaunch: write Hi!') ; - is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\n' ) ; - is( 1, print( $tee "Hoo\n" ), 'teelaunch: write Hoo') ; - is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\nHoo\n' ) ; + $tee = teelaunch( $mysync, 'W/tmp/tests/tests_teelaunch2.txt' ) ; + isa_ok( $tee, 'IO::Tee' , 'tests_teelaunch: W/tmp/tests/tests_teelaunch2.txt' ) ; + is( 1, print( $tee "Hi!\n" ), 'tests_teelaunch: write Hi!') ; + is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\n' ) ; + is( 1, print( $tee "Hoo\n" ), 'tests_teelaunch: write Hoo') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\nHoo\n' ) ; - is( 1, teefinish( $mysync ), 'teefinish: return 1') ; - is( 1, print( $tee "Argh1\n" ), 'teelaunch: write Argh1') ; - is( 1, print( $tee "Argh2\n" ), 'teelaunch: write Argh2') ; - is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is still Hi!\nHoo\n' ) ; - is( 1, teefinish( $mysync ), 'teefinish: still return 1') ; + is( 1, teefinish( $tee ), 'tests_teelaunch: teefinish return 1') ; + is( 1, print( $tee "Argh1\n" ), 'tests_teelaunch: write Argh1') ; + is( 1, print( $tee "Argh2\n" ), 'tests_teelaunch: write Argh2') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'tests_teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is still Hi!\nHoo\n' ) ; + is( 1, teefinish( $tee ), 'tests_teelaunch: teefinish still return 1') ; note( 'Leaving tests_teelaunch()' ) ; return ; -} +} -sub teelaunch +sub teelaunch { - my $mysync = shift ; + my $mysync = shift @ARG ; + my $logfile = shift @ARG ; if ( ! defined( $mysync ) ) { return ; } - my $logfile = $mysync->{logfile} ; - if ( ! $logfile ) { return ; } - logfileprepa( $logfile ) || croak "Error no valid directory to write log file $logfile : $OS_ERROR" ; - + if ( not logfileprepa( $mysync, $logfile ) ) + { + carp( "Error no valid directory to write log file $logfile : $OS_ERROR" ) ; + } + + # To honor -T tainted mode. Could do better... + ( $logfile ) = $logfile =~ m{(.*)}x ; # This is a log file opened during the whole sync ## no critic (InputOutput::RequireBriefOpen) - open my $logfile_handle, '>', $logfile - or croak( "Can not open $logfile for write: $OS_ERROR" ) ; - binmode $logfile_handle, ":encoding(UTF-8)" ; - my $tee = IO::Tee->new( $logfile_handle, \*STDOUT ) ; - $tee->autoflush( 1 ) ; - $mysync->{logfile_handle} = $logfile_handle ; - $mysync->{tee} = $tee ; - return $tee ; + if ( ! open my $logfile_handle, '>', $logfile ) + { + carp( "Can not open $logfile for write: $OS_ERROR" ) ; + return ; + } + else + { + binmode $logfile_handle, ":encoding(UTF-8)" ; + my $tee = IO::Tee->new( $logfile_handle, \*STDOUT ) ; + $tee->autoflush( 1 ) ; + return $tee ; + } } -sub teefinish +sub teefinish { - my $mysync = shift ; - - if ( ! defined( $mysync ) ) { return ; } - - my $tee = $mysync->{tee} ; + my $tee = shift @ARG ; if ( ! defined( $tee ) ) { return ; } if ( 2 == scalar $tee->handles ) { - shift @{*{$tee}}; + my $handle = shift @{*{$tee}} ; + $handle->close ; } else { # nothing } return scalar $tee->handles ; -} +} sub getpwuid_any_os { - my $uid = shift ; + my $uid = shift @ARG ; return( scalar getlogin ) if ( 'MSWin32' eq $OSNAME ) ; # Windows system return( scalar getpwuid $uid ) ; # Unix system @@ -18195,7 +21162,7 @@ sub getpwuid_any_os sub abortifneeded { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( -e $mysync->{ abortfile } ) { myprint( "Asked to terminate by file $mysync->{ abortfile }\n" ) ; @@ -18212,9 +21179,11 @@ sub abortifneeded } } + + sub simulong { - my $mysync = shift ; + my $mysync = shift @ARG ; my $max_seconds = $mysync->{ simulong } ; @@ -18247,7 +21216,7 @@ sub printenv sub unittestssuite { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! ( $mysync->{ tests } or $mysync->{ testsdebug } or $mysync->{ testsunit } ) ) { return ; } @@ -18257,6 +21226,8 @@ sub unittestssuite testsdebug( $mysync ) ; testunitsession( $mysync ) ; + cleanup_mess_from_tests( ) ; + my @summary = $test_builder->summary() ; my @details = $test_builder->details() ; my $nb_tests_run = scalar( @summary ) ; @@ -18269,34 +21240,44 @@ sub unittestssuite "List of failed tests:\n", $tests_failed ) ; return $EXIT_TESTS_FAILED ; } - - cleanup_mess_from_tests( ) ; - return 0 ; } sub cleanup_mess_from_tests { undef @pipemess ; + undef @include ; + undef @exclude ; + undef @folderfirst ; + undef @folderlast ; + undef @h1_folders_all ; + undef %h1_folders_all ; + undef @h2_folders_all ; + undef %h2_folders_all ; + undef @h2_folders_from_1_wanted ; + undef %h2_folders_from_1_all ; + undef %requested_folder ; + return ; } -sub after_get_options +sub after_get_options { - my $mysync = shift ; - my $numopt = shift ; + my $mysync = shift @ARG ; + my $numopt = shift @ARG ; # exit with --help option or no option at all $mysync->{ debug } and myprint( "numopt:$numopt\n" ) ; - if ( $help or not $numopt ) { + if ( $mysync->{ help } or not $numopt ) + { myprint( usage( $mysync ) ) ; exit ; } return ; -} +} sub tests_remove_edging_blanks { @@ -18314,7 +21295,7 @@ sub tests_remove_edging_blanks sub remove_edging_blanks { - my $string = shift ; + my $string = shift @ARG ; if ( ! defined $string ) { return ; @@ -18345,7 +21326,7 @@ sub tests_sanitize sub sanitize { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! defined $mysync ) { return ; @@ -18360,47 +21341,56 @@ sub sanitize sub easyany { - my $mysync = shift ; + my $mysync = shift @ARG ; # Gmail - if ( $mysync->{gmail1} and $mysync->{gmail2} ) { + if ( $mysync->{gmail1} and $mysync->{gmail2} ) + { $mysync->{ debug } and myprint( "gmail1 gmail2\n") ; gmail12( $mysync ) ; return ; } - if ( $mysync->{gmail1} ) { + if ( $mysync->{gmail1} ) + { $mysync->{ debug } and myprint( "gmail1\n" ) ; gmail1( $mysync ) ; } - if ( $mysync->{gmail2} ) { + if ( $mysync->{gmail2} ) + { $mysync->{ debug } and myprint( "gmail2\n" ) ; gmail2( $mysync ) ; } # Office 365 - if ( $mysync->{office1} ) { + if ( $mysync->{office1} ) + { office1( $mysync ) ; } - if ( $mysync->{office2} ) { + if ( $mysync->{office2} ) + { office2( $mysync ) ; } # Exchange - if ( $mysync->{exchange1} ) { + if ( $mysync->{exchange1} ) + { exchange1( $mysync ) ; } - if ( $mysync->{exchange2} ) { + if ( $mysync->{exchange2} ) + { exchange2( $mysync ) ; } # Domino - if ( $mysync->{domino1} ) { + if ( $mysync->{domino1} ) + { domino1( $mysync ) ; } - if ( $mysync->{domino2} ) { + if ( $mysync->{domino2} ) + { domino2( $mysync ) ; } @@ -18408,67 +21398,70 @@ sub easyany } # From and for https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt -sub gmail12 +# globals @exclude @folderlast +sub gmail12 { - my $mysync = shift ; + my $mysync = shift @ARG ; # Gmail at host1 and host2 $mysync->{host1} ||= 'imap.gmail.com' ; $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; $mysync->{host2} ||= 'imap.gmail.com' ; $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; - $mysync->{maxbytespersecond} ||= 20_000 ; # should be less than 10_000 when computed from Gmail documentation - $mysync->{maxbytesafter} ||= 1_000_000_000 ; # In fact it is documented as half: 500_000_000 + $mysync->{ maxbytespersecond } ||= 300_000 ; # should be less than 10_000 when computed from Gmail documentation + $mysync->{ maxbytesafter } ||= 3_000_000_000 ; # In fact it is documented as half: 500_000_000 $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; - $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 0 ; + $mysync->{ skipcrossduplicates } = ( defined $mysync->{ skipcrossduplicates } ) ? $mysync->{ skipcrossduplicates } : 0 ; $mysync->{ synclabels } = ( defined $mysync->{ synclabels } ) ? $mysync->{ synclabels } : 1 ; $mysync->{ resynclabels } = ( defined $mysync->{ resynclabels } ) ? $mysync->{ resynclabels } : 1 ; - push @useheader, 'X-Gmail-Received', 'Message-Id' ; + $mysync->{ idatefromheader } = ( defined $mysync->{ idatefromheader } ) ? $mysync->{ idatefromheader } : 1 ; + push @{ $mysync->{ useheader } } , 'X-Gmail-Received', 'Message-Id' ; push @exclude, '\[Gmail\]$' ; - push @folderlast, '[Gmail]/All Mail' ; + push @folderlast, '[Gmail]/Sent Mail', '[Gmail]/Important', '[Gmail]/Starred', '[Gmail]/Drafts', '[Gmail]/Trash', '[Gmail]/Spam', '[Gmail]/Chats', '[Gmail]/All Mail' ; return ; -} - +} +# globals @folderlast sub gmail1 { - my $mysync = shift ; + my $mysync = shift @ARG ; # Gmail at host2 $mysync->{host1} ||= 'imap.gmail.com' ; $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; - $mysync->{maxbytespersecond} ||= 40_000 ; # should be 30_000 computed from by Gmail documentation - $mysync->{maxbytesafter} ||= 3_000_000_000 ; # + $mysync->{ maxbytespersecond } ||= 300_000 ; # should be 30_000 computed from by Gmail documentation + $mysync->{ maxbytesafter } ||= 3_000_000_000 ; # $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; - $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; + $mysync->{ skipcrossduplicates } = ( defined $mysync->{ skipcrossduplicates } ) ? $mysync->{ skipcrossduplicates } : 1 ; - push @useheader, 'X-Gmail-Received', 'Message-Id' ; + push @{ $mysync->{ useheader } }, 'X-Gmail-Received', 'Message-Id' ; push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; - push @folderlast, '[Gmail]/All Mail' ; + push @folderlast, '[Gmail]/Sent Mail', '[Gmail]/Important', '[Gmail]/Starred', '[Gmail]/Drafts', '[Gmail]/Trash', '[Gmail]/Spam', '[Gmail]/Chats', '[Gmail]/All Mail' ; return ; } +# globals @exclude @folderlast sub gmail2 { - my $mysync = shift ; + my $mysync = shift @ARG ; # Gmail at host2 $mysync->{ host2 } ||= 'imap.gmail.com' ; $mysync->{ ssl2 } = ( defined $mysync->{ ssl2 } ) ? $mysync->{ ssl2 } : 1 ; - $mysync->{ maxbytespersecond } ||= 20_000 ; # should be less than 10_000 computed from by Gmail documentation - $mysync->{ maxbytesafter } ||= 1_000_000_000 ; # In fact it is documented as half: 500_000_000 - + $mysync->{ maxbytespersecond } ||= 300_000 ; # should be less than 10_000 computed from by Gmail documentation + $mysync->{ maxbytesafter } ||= 3_000_000_000 ; # In fact it is documented as half: 500_000_000 $mysync->{ automap } = ( defined $mysync->{ automap } ) ? $mysync->{ automap } : 1 ; - #$skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; $mysync->{ expunge1 } = ( defined $mysync->{ expunge1 } ) ? $mysync->{ expunge1 } : 1 ; $mysync->{ addheader } = ( defined $mysync->{ addheader } ) ? $mysync->{ addheader } : 1 ; - $mysync->{ maxsleep } = ( defined $mysync->{ maxsleep } ) ? $mysync->{ maxsleep } : $MAX_SLEEP ; ; + $mysync->{ maxsleep } = ( defined $mysync->{ maxsleep } ) ? $mysync->{ maxsleep } : $MAX_SLEEP ; + $mysync->{ idatefromheader } = ( defined $mysync->{ idatefromheader } ) ? $mysync->{ idatefromheader } : 1 ; #$mysync->{ maxsize } = ( defined $mysync->{ maxsize } ) ? $mysync->{ maxsize } : $GMAIL_MAXSIZE ; - if ( ! $mysync->{ noexclude } ) { + if ( ! $mysync->{ noexclude } ) + { push @exclude, '\[Gmail\]$' ; } - push @useheader, 'Message-Id' ; + push @{ $mysync->{ useheader } } , 'Message-Id' ; push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; # push @{ $mysync->{ regextrans2 } }, 's/[ ]+/_/g' ; # is now replaced @@ -18478,16 +21471,16 @@ sub gmail2 push @{ $mysync->{ regextrans2 } }, 's,/ +| +/,/,g' ; # push @{ $mysync->{ regextrans2 } }, q{s/['\\^"]/_/g} ; # Verified this - push @folderlast, '[Gmail]/All Mail' ; + push @folderlast, '[Gmail]/Sent Mail', '[Gmail]/Important', '[Gmail]/Starred', '[Gmail]/Drafts', '[Gmail]/Trash', '[Gmail]/Spam', '[Gmail]/Chats', '[Gmail]/All Mail' ; return ; -} +} # From https://imapsync.lamiral.info/FAQ.d/FAQ.Exchange.txt sub office1 { # Office 365 at host1 - my $mysync = shift ; + my $mysync = shift @ARG ; output( $mysync, q{Option --office1 is like: --host1 outlook.office365.com --ssl1 --exclude "^Files$"} . "\n" ) ; output( $mysync, "Option --office1 (cont) : unless overrided with --host1 otherhost --nossl1 --noexclude\n" ) ; @@ -18503,7 +21496,7 @@ sub office1 sub office2 { # Office 365 at host2 - my $mysync = shift ; + my $mysync = shift @ARG ; output( $mysync, qq{Option --office2 is like: --host2 outlook.office365.com --ssl2 --maxsize 45_000_000 --maxmessagespersecond 4\n} ) ; output( $mysync, qq{Option --office2 (cont) : --disarmreadreceipts --regexmess "wrap 10500" --f1f2 "Files=Files_renamed_by_imapsync"\n} ) ; output( $mysync, qq{Option --office2 (cont) : unless overrided with --host2 otherhost --nossl2 ... --nodisarmreadreceipts --noregexmess\n} ) ; @@ -18513,7 +21506,7 @@ sub office2 $mysync->{ maxsize } ||= 45_000_000 ; $mysync->{maxmessagespersecond} ||= 4 ; #push @{ $mysync->{ regexflag } }, 's/\\\\Flagged//g' ; # No problem without! tested 2018_09_10 - $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + $mysync->{ disarmreadreceipts } = ( defined $mysync->{ disarmreadreceipts } ) ? $mysync->{ disarmreadreceipts } : 1 ; # I dislike double negation but here is one if ( ! $mysync->{noregexmess} ) { @@ -18530,7 +21523,7 @@ sub office2 sub exchange1 { # Exchange 2010/2013 at host1 - my $mysync = shift ; + my $mysync = shift @ARG ; output( $mysync, "Option --exchange1 does nothing (except printing this line...)\n" ) ; # Well nothing to do so far return ; @@ -18539,13 +21532,13 @@ sub exchange1 sub exchange2 { # Exchange 2010/2013 at host2 - my $mysync = shift ; + my $mysync = shift @ARG ; output( $mysync, "Option --exchange2 is like: --maxsize 10_000_000 --maxmessagespersecond 4 --disarmreadreceipts\n" ) ; output( $mysync, "Option --exchange2 (cont) : --regexflag del Flagged --regexmess wrap 10500\n" ) ; output( $mysync, "Option --exchange2 (cont) : unless overrided with --maxsize xxx --nodisarmreadreceipts --noregexflag --noregexmess\n" ) ; $mysync->{ maxsize } ||= 10_000_000 ; $mysync->{maxmessagespersecond} ||= 4 ; - $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + $mysync->{ disarmreadreceipts } = ( defined $mysync->{ disarmreadreceipts } ) ? $mysync->{ disarmreadreceipts } : 1 ; # I dislike double negation but here are two if ( ! $mysync->{noregexflag} ) { push @{ $mysync->{ regexflag } }, 's/\\\\Flagged//g' ; @@ -18556,28 +21549,28 @@ sub exchange2 return ; } -sub domino1 +sub domino1 { # Domino at host1 - my $mysync = shift ; + my $mysync = shift @ARG ; $mysync->{ sep1 } = q{\\} ; - $prefix1 = q{} ; + $mysync->{ prefix1 } = q{} ; $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; return ; -} +} -sub domino2 +sub domino2 { # Domino at host1 - my $mysync = shift ; + my $mysync = shift @ARG ; $mysync->{ sep2 } = q{\\} ; - $prefix2 = q{} ; + $mysync->{ prefix2 } = q{} ; $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; push @{ $mysync->{ regextrans2 } }, 's,^Inbox\\\\(.*),$1,i' ; return ; -} +} sub tests_resolv @@ -18618,7 +21611,8 @@ sub resolv if ( ! $host ) { return ; } my $addr ; - if ( defined &Socket::getaddrinfo ) { + if ( defined &Socket::getaddrinfo ) + { $addr = resolv_with_getaddrinfo( $host ) ; return( $addr ) ; } @@ -18640,7 +21634,8 @@ sub resolv_with_getaddrinfo if ( ! $host ) { return ; } my ( $err_getaddrinfo, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; - if ( $err_getaddrinfo ) { + if ( $err_getaddrinfo ) + { myprint( "Cannot getaddrinfo of $host: $err_getaddrinfo\n" ) ; return ; } @@ -18648,10 +21643,13 @@ sub resolv_with_getaddrinfo my @addr ; while( my $ai = shift @res ) { my ( $err_getnameinfo, $ipaddr ) = Socket::getnameinfo( $ai->{addr}, Socket::NI_NUMERICHOST(), Socket::NIx_NOSERV() ) ; - if ( $err_getnameinfo ) { + if ( $err_getnameinfo ) + { myprint( "Cannot getnameinfo of $host: $err_getnameinfo\n" ) ; return ; - }else{ + } + else + { $sync->{ debug } and myprint( "$host => $ipaddr\n" ) ; push @addr, $ipaddr ; my $reverse ; @@ -18698,7 +21696,8 @@ sub resolvrev if ( ! $host ) { return ; } - if ( defined &Socket::getaddrinfo ) { + if ( defined &Socket::getaddrinfo ) + { my $name = resolvrev_with_getaddrinfo( $host ) ; return( $name ) ; } @@ -18713,7 +21712,8 @@ sub resolvrev_with_getaddrinfo if ( ! $host ) { return ; } my ( $err, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; - if ( $err ) { + if ( $err ) + { myprint( "Cannot getaddrinfo of $host: $err\n" ) ; return ; } @@ -18721,7 +21721,8 @@ sub resolvrev_with_getaddrinfo my @name ; while( my $ai = shift @res ) { my ( $err, $reverse ) = Socket::getnameinfo( $ai->{addr}, 0, Socket::NIx_NOSERV() ) ; - if ( $err ) { + if ( $err ) + { myprint( "Cannot getnameinfo of $host: $err\n" ) ; return ; } @@ -18748,7 +21749,7 @@ sub tests_imapsping sub imapsping { - my $host = shift ; + my $host = shift @ARG ; return tcpping( $host, $IMAP_SSL_PORT ) ; } @@ -18779,7 +21780,8 @@ sub tests_tcpping sub tcpping { - if ( 2 != scalar( @ARG ) ) { + if ( 2 != scalar( @ARG ) ) + { return ; } my ( $host, $port ) = @ARG ; @@ -18798,7 +21800,8 @@ sub tcpping my $rtt_approx = sprintf( "%.3f", $rtt ) ; $sync->{ debug } and myprint( "Host $host timeout $mytimeout port $port ok $ping_ok ip $ip acked in $rtt_approx s\n" ) ; $p->close( ) ; - if( $ping_ok ) { + if( $ping_ok ) + { return 1 ; }else{ return 0 ; @@ -18860,77 +21863,195 @@ sub tests_sslcheck return ; } + + sub sslcheck { - my $mysync = shift ; + my $mysync = shift @ARG ; - if ( ! $mysync->{sslcheck} ) { + if ( ! $mysync->{ sslcheck } ) + { return ; } my $nb_on = 0 ; - $mysync->{ debug } and myprint( "sslcheck\n" ) ; + $mysync->{ debug } and myprint( "Doing sslcheck\n" ) ; if ( - ( ! defined $mysync->{port1} ) + ( ! defined $mysync->{ tls1 } ) and - ( ! defined $mysync->{tls1} ) + ( ! defined $mysync->{ ssl1 } ) and - ( ! defined $mysync->{ssl1} ) - and - ( defined $mysync->{host1} ) - ) { - myprint( "Host1: probing ssl on port $IMAP_SSL_PORT ( use --nosslcheck to avoid this ssl probe ) \n" ) ; - if ( probe_imapssl( $mysync->{host1} ) ) { - $mysync->{ssl1} = 1 ; - myprint( "Host1: sslcheck detected open ssl port $IMAP_SSL_PORT so turning ssl on (use --nossl1 --notls1 to turn off SSL and TLS wizardry)\n" ) ; + ( defined $mysync->{ host1 } ) + ) + { + my $port = $mysync->{ port1 } || $IMAP_SSL_PORT ; + myprint( "Host1: probing ssl on port $port ( use --nosslcheck to avoid this ssl probe ) \n" ) ; + if ( probe_imapssl( $mysync->{ host1 }, $port ) ) + { + $mysync->{ ssl1 } = 1 ; + myprint( "Host1: sslcheck detected open ssl port $port so turning ssl on (use --nossl1 --notls1 to turn off SSL and TLS wizardry)\n" ) ; $nb_on++ ; }else{ - myprint( "Host1: sslcheck did not detected open ssl port $IMAP_SSL_PORT. Will use standard $IMAP_PORT port.\n" ) ; + myprint( "Host1: sslcheck did not detected open ssl port $port. Will use standard connection.\n" ) ; } } if ( - ( ! defined $mysync->{port2} ) + ( ! defined $mysync->{ tls2 } ) and - ( ! defined $mysync->{tls2} ) + ( ! defined $mysync->{ ssl2 } ) and - ( ! defined $mysync->{ssl2} ) - and - ( defined $mysync->{host2} ) - ) { - myprint( "Host2: probing ssl on port $IMAP_SSL_PORT ( use --nosslcheck to avoid this ssl probe ) \n" ) ; - if ( probe_imapssl( $mysync->{host2} ) ) { - $mysync->{ssl2} = 1 ; - myprint( "Host2: sslcheck detected open ssl port $IMAP_SSL_PORT so turning ssl on (use --nossl2 --notls2 to turn off SSL and TLS wizardry)\n" ) ; + ( defined $mysync->{ host2 } ) + ) + { + my $port = $mysync->{ port2 } || $IMAP_SSL_PORT ; + myprint( "Host2: probing ssl on port $port ( use --nosslcheck to avoid this ssl probe ) \n" ) ; + if ( probe_imapssl( $mysync->{ host2 }, $port ) ) { + $mysync->{ ssl2 } = 1 ; + myprint( "Host2: sslcheck detected open ssl port $port so turning ssl on (use --nossl2 --notls2 to turn off SSL and TLS wizardry)\n" ) ; $nb_on++ ; }else{ - myprint( "Host2: sslcheck did not detected open ssl port $IMAP_SSL_PORT. Will use standard $IMAP_PORT port.\n" ) ; + myprint( "Host2: sslcheck did not detected open ssl port $port. Will use standard connection.\n" ) ; } } return $nb_on ; } + +sub tests_setlog +{ + note( 'Entering tests_setlog()' ) ; + + is( undef, setlog( ), 'tests_setlog: no args => undef' ) ; + + do { + # Not in cgi context + delete local $ENV{ SERVER_SOFTWARE } ; + delete local $ENV{ NET_SERVER_SOFTWARE } ; + my $mysync = { } ; + $mysync->{ dockercontext } = 0 ; + is( 1, setlog( $mysync ), 'tests_setlog: default => log is on' ) ; + myprint( output( $mysync ) ) ; + } ; + do { + # In Docker+CGI context + local $ENV{ SERVER_SOFTWARE } = 'under imapsync' ; + my $mysync = { } ; + $mysync->{ dockercontext } = 1 ; + is( 1, setlog( $mysync ), 'tests_setlog: SERVER_SOFTWARE set + Docker => log is on' ) ; + myprint( output( $mysync ) ) ; + } ; + do { + # In Docker+CGI context + local $ENV{ NET_SERVER_SOFTWARE } = 'Net::Server::HTTP' ; + my $mysync = { } ; + $mysync->{ dockercontext } = 1 ; + is( 1, setlog( $mysync ), 'tests_setlog: NET_SERVER_SOFTWARE set + Docker => log is on' ) ; + myprint( output( $mysync ) ) ; + } ; + do { + # Not in cgi context + Docker context + delete local $ENV{ SERVER_SOFTWARE } ; + delete local $ENV{ NET_SERVER_SOFTWARE } ; + my $mysync = { } ; + $mysync->{ dockercontext } = 1 ; + is( 0, setlog( $mysync ), 'tests_setlog: Docker context, not CGI => log is off' ) ; + myprint( output( $mysync ) ) ; + } ; + do { + # In cgi context + local $ENV{ SERVER_SOFTWARE } = 'under imapsync' ; + my $mysync = { } ; + is( 1, setlog( $mysync ), 'tests_setlog: CGI context => log is on' ) ; + myprint( output( $mysync ) ) ; + } ; + note( 'Leaving tests_setlog()' ) ; + return ; +} + + + +sub setlog +{ + my $mysync = shift @ARG ; + + if ( ! defined( $mysync ) ) { return ; } + + if ( defined $mysync->{ log } ) + { + if ( $mysync->{ log } ) + { + if ( under_docker_context( $mysync ) ) + { + output( $mysync, "Logging in Docker context. Be sure you added access to the logfile with a mount or similar. See https://docs.docker.com/storage/volumes/\n" ) ; + } + else + { + output( $mysync, "Logging is on. To turn off logging, use --nolog\n" ) ; + } + } + else + { + output( $mysync, "Logging is off. To turn on logging, use --log\n" ) ; + } + } + elsif ( under_docker_context( $mysync ) and ( under_Net_Server_HTTP( $mysync ) or under_cgi_context( $mysync ) ) ) + { + $mysync->{ log } = 1 ; + output( $mysync, "Logging to a logfile in Docker+CGI context, by default. To turn off logging, use --nolog\n" ) ; + } + elsif ( under_docker_context( $mysync ) ) + { + $mysync->{ log } = 0 ; + output( $mysync, "No log to a logfile in basic Docker context, by default. To turn on logging, use --log\n" ) ; + } + else + { + output( $mysync, "Logging to a logfile by default. To turn off logging, use --nolog\n" ) ; + $mysync->{ log } = 1 ; + } + return $mysync->{ log } ; +} + + + sub testslive_init { - my $mysync = shift ; - $mysync->{host1} ||= 'test1.lamiral.info' ; - $mysync->{user1} ||= 'test1' ; - $mysync->{password1} ||= 'secret1' ; - $mysync->{host2} ||= 'test2.lamiral.info' ; - $mysync->{user2} ||= 'test2' ; - $mysync->{password2} ||= 'secret2' ; + my $mysync = shift @ARG ; + $mysync->{ host1 } ||= 'test1.lamiral.info' ; + $mysync->{ user1 } ||= 'test1' ; + $mysync->{ password1 } ||= 'secret1' ; + $mysync->{ host2 } ||= 'test2.lamiral.info' ; + $mysync->{ user2 } ||= 'test2' ; + $mysync->{ password2 } ||= 'secret2' ; return ; } +sub testslive2_init +{ + my $mysync = shift @ARG ; + $mysync->{ host1 } ||= 'test2.lamiral.info' ; + $mysync->{ user1 } ||= 'test2' ; + $mysync->{ password1 } ||= 'secret2' ; + $mysync->{ host2 } ||= 'test1.lamiral.info' ; + $mysync->{ user2 } ||= 'test1' ; + $mysync->{ password2 } ||= 'secret1' ; + return ; +} + + + + + sub testslive6_init { - my $mysync = shift ; - $mysync->{host1} ||= 'ks6ipv6.lamiral.info' ; - $mysync->{user1} ||= 'test1' ; - $mysync->{password1} ||= 'secret1' ; - $mysync->{host2} ||= 'ks6ipv6.lamiral.info' ; - $mysync->{user2} ||= 'test2' ; - $mysync->{password2} ||= 'secret2' ; + my $mysync = shift @ARG ; + $mysync->{ host1 } ||= 'test1ipv6.lamiral.info' ; + $mysync->{ user1 } ||= 'test1' ; + $mysync->{ password1 } ||= 'secret1' ; + $mysync->{ host2 } ||= 'test2ipv6.lamiral.info' ; + $mysync->{ user2 } ||= 'test2' ; + $mysync->{ password2 } ||= 'secret2' ; return ; } @@ -18956,7 +22077,7 @@ sub tests_backslash_caret sub backslash_caret { - my $string = shift ; + my $string = shift @ARG ; $string =~ s{\\ $ }{^}gxms ; @@ -18968,20 +22089,24 @@ sub tests_split_around_equal note( 'Entering tests_split_around_equal()' ) ; is( undef, split_around_equal( ), 'split_around_equal: no args => undef' ) ; - is_deeply( { toto => 'titi' }, { split_around_equal( 'toto=titi' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; - is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B=C=D' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; + is_deeply( { toto => 'titi' }, { split_around_equal( 'toto=titi' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; + is_deeply( { toto => undef }, { split_around_equal( 'toto' ) }, 'split_around_equal: tototiti => toto => undef' ) ; + is_deeply( { toto => '' }, { split_around_equal( 'toto=' ) }, 'split_around_equal: tototiti => toto= => empty' ) ; is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B', 'C=D' ) }, 'split_around_equal: A=B C=D => A => B, C=>D' ) ; + is_deeply( { A => 'B', C => 'D', E => 'F' }, { split_around_equal( 'A=B', 'C=D', 'E=F' ) }, 'split_around_equal: A=B C=D => A => B, C=>D' ) ; + is_deeply( { A => 'B=C' }, { split_around_equal( 'A=B=C' ) }, 'split_around_equal: A=B=C => A => B=C' ) ; + is_deeply( { A => 'B=C=D' }, { split_around_equal( 'A=B=C=D' ) }, 'split_around_equal: A=B=C=D => A => B=C=D' ) ; note( 'Leaving tests_split_around_equal()' ) ; return ; } -sub split_around_equal -{ +sub split_around_equal +{ if ( ! @ARG ) { return ; } ; - return map { split /=/mxs, $_ } @ARG ; + return map { split( /=/mxs, $_, 2 ) } @ARG ; -} +} @@ -19043,9 +22168,9 @@ sub tests_sig_install # sub sig_install { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync ) { return ; } - my $mysubname = shift ; + my $mysubname = shift @ARG ; if ( ! $mysubname ) { return ; } if ( ! @ARG ) { return ; } @@ -19057,7 +22182,7 @@ sub sig_install $mysync->{ debugsig } and myprint( "In sig_install with sub $mysubname and signal @ARG\n" ) ; my $subsignal = sub { - my $signame = shift ; + my $signame = shift @ARG ; $mysync->{ debugsig } and myprint( "In subsignal with $signame and $mysubname\n" ) ; &$mysub( $mysync, $signame ) ; } ; @@ -19074,7 +22199,7 @@ sub sig_install sub tototo { - my $mysync = shift ; + my $mysync = shift @ARG ; myprint("In tototo with @ARG\n" ) ; $mysync->{ tototo_calls } += 1 ; return ; @@ -19142,7 +22267,7 @@ sub tests_toggle_sleep sub toggle_sleep { - my $mysync = shift ; + my $mysync = shift @ARG ; myprint("In toggle_sleep with @ARG\n" ) ; @@ -19156,7 +22281,7 @@ sub toggle_sleep sub mypod2usage { - my $fh_pod2usage = shift ; + my $fh_pod2usage = shift @ARG ; pod2usage( -exitval => 'NOEXIT', @@ -19173,7 +22298,7 @@ sub mypod2usage sub usage { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! defined $mysync ) { return ; } @@ -19215,7 +22340,7 @@ sub tests_usage sub usage_footer { - my $mysync = shift ; + my $mysync = shift @ARG ; my $footer = q{} ; @@ -19273,8 +22398,8 @@ sub setvalfromcgikey my ( $mysync, $mycgi, $key, $val ) = @ARG ; my $badthings = 0 ; - - + + my ( $name, $type, $struct ) ; if ( $key !~ m/^([\w\d\|]+)([=:][isf])?([\+!\@\%])?$/mxs ) { @@ -19287,7 +22412,7 @@ sub setvalfromcgikey $type = $2 ; # = or : followed by i or s or f $struct = $3 ; # + or ! or @ or % } - + if ( ( $struct || q{} ) eq '+' ) { ${$val} = $mycgi->param( $name ) ; # "Incremental" integer @@ -19301,7 +22426,7 @@ sub setvalfromcgikey { setvalfromhash( $val, $type, @values ) ; } - else + else { setvalfromlist( $mysync, $val, $name, $type, $struct, @values ) ; } @@ -19310,7 +22435,7 @@ sub setvalfromcgikey { setvalfromcheckbox( $mysync, $mycgi, $key, $name, $val ) ; } - + return $badthings ; } @@ -19344,7 +22469,7 @@ sub setvalfromlist else { } - + return ; } sub setvalfromhash @@ -19371,11 +22496,11 @@ sub setvalfromhash { %{ ${$val} } = %values ; } - else + else { %{$val} = %values ; } - + return ; } @@ -19383,7 +22508,7 @@ sub setvalfromhash sub setvalfromcheckbox { my ( $mysync, $mycgi, $key, $name, $val ) = @ARG ; - + # Checkbox # --noname is set by name=0 or name= my $value = $mycgi->param( $name ) ; @@ -19413,7 +22538,7 @@ sub myGetOptions # https://metacpan.org/release/Getopt-Long-CGI # So this sub function is under the same license as Getopt-Long-CGI Luke Ross wants it, # which was Perl 5.6 or later licenses at the date of the copy. - # It also applies for the sub functions called from this one. + # It also applies for the sub setvalfromcgikey called from this one. my $mysync = shift @ARG ; my $arguments_ref = shift @ARG ; @@ -19437,7 +22562,7 @@ sub myGetOptions $badthings += setvalfromcgikey( $mysync, $mycgi, $key, $val ) ; } - + if ( $badthings ) { return ; # undef or () } @@ -19448,51 +22573,56 @@ sub myGetOptions -sub tests_get_options_extra +sub tests_get_options_extra_from_file { - note( 'Entering tests_get_options_extra()' ) ; - - is( undef, get_options_extra( ), 'get_options_extra: no args => undef' ) ; + note( 'Entering tests_get_options_extra_from_file()' ) ; + is( undef, get_options_extra_from_file( ), 'tests_get_options_extra_from_file: no args => undef' ) ; + my $mysync = { } ; - is( undef, get_options_extra( $mysync ), 'get_options_extra: undef => undef' ) ; - + is( undef, get_options_extra_from_file( $mysync ), 'tests_get_options_extra_from_file: undef => undef' ) ; + my $cwd_save = getcwd( ) ; - - ok( (-d 'W/tmp/tests/options_extra/' or mkpath( 'W/tmp/tests/options_extra/' )), 'get_options_extra: mkpath W/tmp/tests/options_extra/' ) ; - + + ok( (-d 'W/tmp/tests/options_extra/' or mkpath( 'W/tmp/tests/options_extra/' )), 'tests_get_options_extra_from_file: mkpath W/tmp/tests/options_extra/' ) ; + chdir 'W/tmp/tests/options_extra/' ; + + is( '--debugimap1', string_to_file( '--debugimap1', 'options_extra.txt' ), 'tests_get_options_extra_from_file: string_to_file filling options_extra.txt with --debugimap1' ) ; + is( '--debugimap1', file_to_string( 'options_extra.txt' ), 'tests_get_options_extra_from_file: reading options_extra.txt is --debugimap1' ) ; + is( '', get_options_extra_from_file( $mysync ), 'tests_get_options_extra_from_file: --debugimap1 in options_extra.txt => nothing left, empty string return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_extra_from_file: --debugimap1 in options_extra.txt => ok, acc1->debugimap = 1' ) ; + + is( '--tls1 proutcaca', string_to_file( '--tls1 proutcaca', 'options_extra.txt' ), 'tests_get_options_extra_from_file: string_to_file filling options_extra.txt with --tls1 proutcaca' ) ; + is( 'proutcaca', get_options_extra_from_file( $mysync ), 'tests_get_options_extra_from_file: --tls1 proutcaca in options_extra.txt => proutcaca left, proutcaca return' ) ; + is( 1, $mysync->{ tls1 }, 'tests_get_options_extra_from_file: --tls1 proutcaca" => tls1 = 1' ) ; - is( '--debugimap1', string_to_file( '--debugimap1', 'options_extra.txt' ), 'get_options_extra: string_to_file filling options_extra.txt with --debugimap1' ) ; - - is( '--debugimap1', file_to_string( 'options_extra.txt' ), 'get_options_extra: reading options_extra.txt is --debugimap1' ) ; - - is( '', get_options_extra( $mysync ), 'get_options_extra: --debugimap1 in options_extra.txt => nothing left, empty string return' ) ; - - is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_extra: --debugimap1 in options_extra.txt => ok, acc1->debugimap = 1' ) ; - - is( '--tls1 proutcaca', string_to_file( '--tls1 proutcaca', 'options_extra.txt' ), 'get_options_extra: string_to_file filling options_extra.txt with --tls1 proutcaca' ) ; - - is( 'proutcaca', get_options_extra( $mysync ), 'get_options_extra: --tls1 proutcaca in options_extra.txt => proutcaca left, proutcaca return' ) ; + is( '--tls2 --proutcaca pipi', string_to_file( '--tls2 --proutcaca pipi', 'options_extra.txt' ), 'tests_get_options_extra_from_file: string_to_file filling options_extra.txt with --tls2 --proutcaca pipi' ) ; + is( '--proutcaca pipi', get_options_extra_from_file( $mysync ), 'tests_get_options_extra_from_file: --tls2 --proutcaca pipi in options_extra.txt => --proutcaca pipi return' ) ; + is( 1, $mysync->{ tls2 }, 'tests_get_options_extra_from_file: --tls2 proutcaca" => tls2 = 1' ) ; chdir $cwd_save ; - - note( 'Leaving tests_get_options_extra()' ) ; + + note( 'Leaving tests_get_options_extra_from_file()' ) ; return ; } -sub get_options_extra +sub get_options_extra_from_file { my $mysync = shift @ARG ; - + if ( ! defined $mysync ) { return ; } - + if ( -f -r 'options_extra.txt' ) { my $cwd = getcwd( ) ; my $string = firstline( 'options_extra.txt' ) ; my $rest = get_options_from_string( $mysync, $string ) ; - output( $mysync, "Reading extra options from file options_extra.txt (cwd: $cwd) : $string\n" ) ; + output( $mysync, "Reading extra options from file options_extra.txt (cwd: $cwd): $string\n" ) ; + if ( $rest ) + { + output( $mysync, "The following options were ignored from file options_extra.txt: $rest\n" ) ; + } return $rest ; } else @@ -19502,62 +22632,143 @@ sub get_options_extra } + +sub tests_get_options_extra_from_param +{ + note( 'Entering tests_get_options_extra_from_param()' ) ; + + is( undef, get_options_extra_from_param( ), 'tests_get_options_extra_from_param: no args => undef' ) ; + + my $mysync = { } ; + is( undef, get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: undef => undef' ) ; + + + $mysync->{ extra } = [ '--debugimap1' ] ; + is( '', get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: --extra --debugimap1 => nothing left, empty string return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_extra_from_param: --debugimap1 in options_extra.txt => ok, acc1->debugimap = 1' ) ; + + $mysync->{ extra } = [ '--tls1 proutcaca' ] ; + is( 'proutcaca', get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: --extra "--tls1 proutcaca" => proutcaca left, proutcaca return' ) ; + is( 1, $mysync->{ tls1 }, 'tests_get_options_extra_from_param: --extra "--tls1 proutcaca" => tls1 = 1' ) ; + + $mysync->{ extra } = [ '--tls1 --proutcaca' ] ; + is( '--proutcaca', get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: --extra "--tls1 --proutcaca" => --proutcaca left, --proutcaca return' ) ; + is( 1, $mysync->{ tls1 }, 'tests_get_options_extra_from_param: --extra "--tls1 --proutcaca" => tls1 = 1' ) ; + + $mysync->{ extra } = [ '--oauthaccesstoken1 proutcaca' ] ; + is( '', get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: --extra "--oauthaccesstoken1 proutcaca" => nothing left, empty string return' ) ; + is( 'proutcaca', $mysync->{ acc1 }->{ oauthaccesstoken }, 'tests_get_options_extra_from_param: --extra "--oauthaccesstoken1 proutcaca" => ->{ acc1 }->{ oauthaccesstoken } = proutcaca' ) ; + + $mysync->{ extra } = [ '--folder "IN BOX" --folder JOE' ] ; + is( '', get_options_extra_from_param( $mysync ), 'tests_get_options_extra_from_param: --folder "IN BOX" --folder JOE => ok, empty string return' ) ; + is_deeply( [ 'IN BOX', 'JOE' ], [@{$mysync->{ folder }}], 'tests_get_options_extra_from_param: "IN BOX" "JOE"' ) ; + note( 'Leaving tests_get_options_extra_from_param()' ) ; + return ; +} + + +sub get_options_extra_from_param +{ + my $mysync = shift @ARG ; + + if ( ! defined $mysync ) { return ; } + + if ( $mysync->{ extra } ) + { + my $string = join( '', @{$mysync->{ extra }} ) ; + my $rest = get_options_from_string( $mysync, $string ) ; + output( $mysync, "Reading or overwriting options from parameter --extra: $string\n" ) ; + if ( $rest ) + { + output( $mysync, "The following options were ignored from parameter --extra: $rest\n" ) ; + } + return $rest ; + } + else + { + return ; + } +} + + + + sub tests_get_options_from_string { note( 'Entering tests_get_options_from_string()' ) ; - is( undef, get_options_from_string( ), 'get_options_from_string: no args => undef' ) ; + is( undef, get_options_from_string( ), 'tests_get_options_from_string: no args => undef' ) ; my $mysync = { } ; - is( undef, get_options_from_string( $mysync ), 'get_options_from_string: undef => undef' ) ; - - is( '', get_options_from_string( $mysync, '--debugimap1' ), - 'get_options_from_string: --debugimap1 => ok, nothing left, empty string return' ) ; - is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; - + is( undef, get_options_from_string( $mysync ), 'tests_get_options_from_string: undef => undef' ) ; + + is( '', get_options_from_string( $mysync, '--debugimap1' ), + 'tests_get_options_from_string: --debugimap1 => ok, nothing left, empty string return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; + $mysync = { } ; # reset - is( 'caca', get_options_from_string( $mysync, '--debugimap1 caca' ), - 'get_options_from_string: --debugimap1 caca => ok, caca left, caca return' ) ; - is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; - - is( 'popo roro', get_options_from_string( $mysync, '--debugimap2 popo roro' ), - 'get_options_from_string: --debugimap1 popo roro => ok, popo roro left, popo roro return' ) ; - is( 1, $mysync->{ acc2 }->{ debugimap }, 'get_options_from_string: --debugimap2 popo roro => ok, acc2->debugimap = 1' ) ; - is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: acc1->debugimap = 1 still' ) ; - - is( '', get_options_from_string( $mysync, '--nodebugimap1 --debugflags --errorsmax 2' ), - 'get_options_from_string: --nodebugimap1 --debugflags --errorsmax 2 => ok, empty string return' ) ; - - is( 0, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: acc1->debugimap = 0 now' ) ; - is( 1, $debugflags, 'get_options_from_string: debugflags = 1 now' ) ; - is( 2, $mysync->{ errorsmax }, 'get_options_from_string: mysync->errorsmax = 2 now' ) ; - - is( '', get_options_from_string( $mysync, '--folder "IN BOX" --folder JOE' ), - 'get_options_from_string: --folder "IN BOX" --folder JOE => ok, empty string return' ) ; - - is_deeply( [ 'IN BOX', 'JOE' ], [@{$mysync->{ folder }}], 'get_options_from_string: "IN BOX" "JOE"' ) ; - - is( '', get_options_from_string( $mysync, '--debugflags --koko' ), - 'get_options_from_string: --debugflags --koko => ok, empty string return, with "Unknown option: koko" on STDERR' ) ; + is( 'caca', get_options_from_string( $mysync, '--debugimap1 caca' ), + 'tests_get_options_from_string: --debugimap1 caca => ok, caca left, caca return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; + + is( 'popo roro', get_options_from_string( $mysync, '--debugimap2 popo roro' ), + 'tests_get_options_from_string: --debugimap1 popo roro => ok, popo roro left, popo roro return' ) ; + is( 1, $mysync->{ acc2 }->{ debugimap }, 'tests_get_options_from_string: --debugimap2 popo roro => ok, acc2->debugimap = 1' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_from_string: acc1->debugimap = 1 still' ) ; + is( '', get_options_from_string( $mysync, '--nodebugimap1 --debugflags --errorsmax 2' ), + 'tests_get_options_from_string: --nodebugimap1 --debugflags --errorsmax 2 => ok, empty string return' ) ; + + is( 0, $mysync->{ acc1 }->{ debugimap }, 'tests_get_options_from_string: acc1->debugimap = 0 now' ) ; + is( 1, $mysync->{ debugflags }, 'tests_get_options_from_string: debugflags = 1 now' ) ; + is( 2, $mysync->{ errorsmax }, 'tests_get_options_from_string: mysync->errorsmax = 2 now' ) ; + + is( '', get_options_from_string( $mysync, '--folder "IN BOX" --folder JOE' ), + 'tests_get_options_from_string: --folder "IN BOX" --folder JOE => ok, empty string return' ) ; + is_deeply( [ 'IN BOX', 'JOE' ], [@{$mysync->{ folder }}], 'tests_get_options_from_string: "IN BOX" "JOE"' ) ; + + is( '--koko', get_options_from_string( $mysync, '--debugflags --koko' ), + 'tests_get_options_from_string: --debugflags --koko => --koko' ) ; + + # string on multi-lines is ok, thanks Getopt::Long! + is( '', get_options_from_string( $mysync, '--folder "IN BOX ONE" + --folder JOE' ), + 'tests_get_options_from_string: --folder "IN BOX" --folder JOE => ok, empty string return' ) ; + is_deeply( [ 'IN BOX ONE', 'JOE' ], [@{$mysync->{ folder }}], 'tests_get_options_from_string: "IN BOX" "JOE"' ) ; + note( 'Leaving tests_get_options_from_string()' ) ; return ; } -sub get_options_from_string +# Be carefull that options here come after the cgi cleanup with get_options_extra_from_param() +# so do not add back dangerous options here. +sub get_options_from_string { my $mysync = shift @ARG ; my $mystring = shift @ARG ; - + if ( ! defined $mystring ) { return ; } - + my ( $ret, $args ) = Getopt::Long::GetOptionsFromString( $mystring, + 'abort' => \$mysync->{ abort }, + 'abortbyfile' => \$mysync->{ abortbyfile }, + 'host1=s' => \$mysync->{ host1 }, + 'host2=s' => \$mysync->{ host2 }, + 'user1=s' => \$mysync->{ user1 }, + 'user2=s' => \$mysync->{ user2 }, + 'password1=s' => \$mysync->{ password1 }, + 'password2=s' => \$mysync->{ password2 }, + 'dry!' => \$mysync->{ dry }, + 'dry1!' => \$mysync->{ dry1 }, + 'version' => \$mysync->{ version }, 'debugimap!' => \$mysync->{ debugimap }, 'debugimap1!' => \$mysync->{ acc1 }->{ debugimap }, 'debugimap2!' => \$mysync->{ acc2 }->{ debugimap }, - 'debugflags!' => \$debugflags, + 'debugflags!' => \$mysync->{ debugflags }, 'debugsleep=f' => \$mysync->{ debugsleep }, 'errorsmax=i' => \$mysync->{ errorsmax }, 'folder=s@' => \$mysync->{ folder }, + 'f1f2=s@' => \$mysync->{ f1f2 }, + 'folderrec=s@' => \$mysync->{ folderrec }, 'timeout=f' => \$mysync->{ timeout }, 'timeout1=f' => \$mysync->{ acc1 }->{ timeout }, 'timeout2=f' => \$mysync->{ acc2 }->{ timeout }, @@ -19565,16 +22776,85 @@ sub get_options_from_string 'keepalive2!' => \$mysync->{ acc2 }->{ keepalive }, 'reconnectretry1=i' => \$mysync->{ acc1 }->{ reconnectretry }, 'reconnectretry2=i' => \$mysync->{ acc2 }->{ reconnectretry }, + 'sslcheck!' => \$mysync->{ sslcheck }, 'ssl1!' => \$mysync->{ ssl1 }, 'ssl2!' => \$mysync->{ ssl2 }, 'tls1!' => \$mysync->{ tls1 }, 'tls2!' => \$mysync->{ tls2 }, + 'ssl1_ssl_version=s' => \$mysync->{ acc1 }->{ sslargs }->{ SSL_version }, + 'ssl2_ssl_version=s' => \$mysync->{ acc2 }->{ sslargs }->{ SSL_version }, + 'sslargs1=s%' => \$mysync->{ acc1 }->{ sslargs }, + 'sslargs2=s%' => \$mysync->{ acc2 }->{ sslargs }, + 'justbanner!' => \$mysync->{ justbanner }, + 'justlogin!' => \$mysync->{ justlogin }, + 'justconnect!' => \$mysync->{ justconnect }, + 'addheader!' => \$mysync->{ addheader }, + 'automap!' => \$mysync->{ automap }, + 'justautomap!' => \$mysync->{ justautomap }, + 'gmail1' => \$mysync->{ gmail1 }, + 'gmail2' => \$mysync->{ gmail2 }, + 'office1' => \$mysync->{ office1 }, + 'office2' => \$mysync->{ office2 }, + 'exchange1' => \$mysync->{ exchange1 }, + 'exchange2' => \$mysync->{ exchange2 }, + 'domino1' => \$mysync->{ domino1 }, + 'domino2' => \$mysync->{ domino2 }, 'compress1!' => \$mysync->{ acc1 }->{ compress }, 'compress2!' => \$mysync->{ acc2 }->{ compress }, + 'oauthaccesstoken1=s' => \$mysync->{ acc1 }->{ oauthaccesstoken }, + 'oauthaccesstoken2=s' => \$mysync->{ acc2 }->{ oauthaccesstoken }, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'skipcrossduplicates!' => \$mysync->{ skipcrossduplicates }, + 'debugcrossduplicates!' => \$mysync->{ debugcrossduplicates }, + 'useheader=s@' => \$mysync->{ useheader }, + + 'testslive!' => \$mysync->{ testslive }, + 'testslive2!' => \$mysync->{ testslive2 }, + 'testslive6!' => \$mysync->{ testslive6 }, + 'releasecheck!' => \$mysync->{ releasecheck }, + 'simulong=f' => \$mysync->{ simulong }, + 'debugsleep=f' => \$mysync->{ debugsleep }, + 'subfolder1=s' => \$mysync->{ subfolder1 }, + 'subfolder2=s' => \$mysync->{ subfolder2 }, + 'justfolders!' => \$mysync->{ justfolders }, + 'justfoldersizes!' => \$mysync->{ justfoldersizes }, + + + 'tail!' => \$mysync->{ tail }, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'exitonload!' => \$mysync->{ exitonload }, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'skipcrossduplicates!' => \$mysync->{ skipcrossduplicates }, + 'debugcrossduplicates!'=> \$mysync->{ debugcrossduplicates }, + 'log!' => \$mysync->{ log }, + 'loglogfile!' => \$mysync->{ loglogfile }, + 'emailreportfrom=s' => \$mysync->{ email_report_from }, + 'emailreport1!' => \$mysync->{ emailreport1 }, + 'emailreport2!' => \$mysync->{ emailreport2 }, + + + + 'delete1!' => \$mysync->{ delete1 }, + 'expunge1!' => \$mysync->{ expunge1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'expunge2!' => \$mysync->{ expunge2 }, + 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, + 'delete2duplicates!' => \$mysync->{ delete2duplicates }, + 'delete1emptyfolders' => \$mysync->{ delete1emptyfolders }, + 'delete2folders!' => \$mysync->{ delete2folders }, + + 'minsize=i' => \$mysync->{ minsize }, + 'maxsize=i' => \$mysync->{ maxsize }, + 'maxage=f' => \$mysync->{ maxage }, + 'minage=f' => \$mysync->{ minage }, + 'search=s' => \$mysync->{ search }, + 'search1=s' => \$mysync->{ search1 }, + 'search2=s' => \$mysync->{ search2 }, + ) ; my $left = join( ' ', @$args ) ; return $left ; -} +} @@ -19586,7 +22866,7 @@ sub tests_get_options_cgi_context note( 'Entering tests_get_options_cgi_context()' ) ; -# Temporary, have to think harder about testing CGI context in command line --tests + # Temporary, have to think harder about testing CGI context in command line --tests # API: # * input arguments: two ways, command line or CGI # * the program arguments @@ -19605,92 +22885,88 @@ sub tests_get_options_cgi_context # skip all next tests if the CGI module is not available SKIP: { - if ( ! eval { require CGI ; } ) { + if ( ! eval { require CGI ; } ) + { skip( "CGI Perl module is not installed", 19 ) ; } CGI->import( qw( -no_debug -utf8 ) ) ; - is( undef, get_options( $mysync ), 'get_options cgi context: no CGI param => undef' ) ; - # Testing boolean - $mysync->{cgi} = CGI->new( 'version=on&debugenv=on' ) ; - local $ENV{'QUERY_STRING'} = 'version=on&debugenv=on' ; - is( 22, get_options( $mysync ), 'get_options cgi context: QUERY_STRING => 22' ) ; - is( 'on', $mysync->{ version }, 'get_options cgi context: --version => on' ) ; - # debugenv is not allowed in cgi context - is( undef, $mysync->{debugenv}, 'get_options cgi context: $mysync->{debugenv} => undef' ) ; + is( undef, get_options( $mysync ), 'get_options cgi context: no CGI param => undef' ) ; + # Testing boolean + $mysync->{cgi} = CGI->new( 'version=on&debugenv=on' ) ; + local $ENV{'QUERY_STRING'} = 'version=on&debugenv=on' ; + is( 22, get_options( $mysync ), 'get_options cgi context: QUERY_STRING => 22' ) ; + is( 'on', $mysync->{ version }, 'get_options cgi context: --version => on' ) ; + # debugenv is not allowed in cgi context + is( undef, $mysync->{debugenv}, 'get_options cgi context: $mysync->{debugenv} => undef' ) ; - # QUERY_STRING in this test is only for return value of get_options - # Have to think harder, GET/POST context, is this return value a good thing? - local $ENV{'QUERY_STRING'} = 'host1=test1.lamiral.info&user1=test1' ; - $mysync->{cgi} = CGI->new( 'host1=test1.lamiral.info&user1=test1' ) ; - is( 36, get_options( $mysync, ), 'get_options cgi context: QUERY_STRING => 36' ) ; - is( 'test1', $mysync->{user1}, 'get_options cgi context: $mysync->{user1} => test1' ) ; - #local $ENV{'QUERY_STRING'} = undef ; + # QUERY_STRING in this test is only for return value of get_options + # Have to think harder, GET/POST context, is this return value a good thing? + local $ENV{'QUERY_STRING'} = 'host1=test1.lamiral.info&user1=test1' ; + $mysync->{cgi} = CGI->new( 'host1=test1.lamiral.info&user1=test1' ) ; + is( 36, get_options( $mysync, ), 'get_options cgi context: QUERY_STRING => 36' ) ; + is( 'test1', $mysync->{user1}, 'get_options cgi context: $mysync->{user1} => test1' ) ; + #local $ENV{'QUERY_STRING'} = undef ; - # Testing s@ as ref - $mysync->{cgi} = CGI->new( 'folder=fd1' ) ; - get_options( $mysync ) ; - is_deeply( [ 'fd1' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1' ) ; - $mysync->{cgi} = CGI->new( 'folder=fd1&folder=fd2' ) ; - get_options( $mysync ) ; - is_deeply( [ 'fd1', 'fd2' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1, fd2' ) ; + # Testing s@ as ref + $mysync->{cgi} = CGI->new( 'folder=fd1' ) ; + get_options( $mysync ) ; + is_deeply( [ 'fd1' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1' ) ; + $mysync->{cgi} = CGI->new( 'folder=fd1&folder=fd2' ) ; + get_options( $mysync ) ; + is_deeply( [ 'fd1', 'fd2' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1, fd2' ) ; - # Testing % - $mysync->{cgi} = CGI->new( 'f1f2h=s1=d1&f1f2h=s2=d2&f1f2h=s3=d3' ) ; - get_options( $mysync ) ; + # Testing % + $mysync->{cgi} = CGI->new( 'f1f2h=s1=d1&f1f2h=s2=d2&f1f2h=s3=d3' ) ; + get_options( $mysync ) ; - is_deeply( { 's1' => 'd1', 's2' => 'd2', 's3' => 'd3' }, - $mysync->{f1f2h}, 'get_options cgi context: f1f2h => s1=d1 s2=d2 s3=d3' ) ; + is_deeply( { 's1' => 'd1', 's2' => 'd2', 's3' => 'd3' }, + $mysync->{f1f2h}, 'get_options cgi context: f1f2h => s1=d1 s2=d2 s3=d3' ) ; - # Testing boolean ! with --noxxx, doesnot work - $mysync->{cgi} = CGI->new( 'nodry=on' ) ; - get_options( $mysync ) ; - is( undef, $mysync->{dry}, 'get_options cgi context: --nodry => $mysync->{dry} => undef' ) ; + # Testing boolean ! with --noxxx, doesnot work + $mysync->{cgi} = CGI->new( 'nodry=on' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{ dry }, 'get_options cgi context: --nodry => $mysync->{ dry } => undef' ) ; - $mysync->{cgi} = CGI->new( 'host1=example.com' ) ; - get_options( $mysync ) ; - is( 'example.com', $mysync->{host1}, 'get_options cgi context: --host1=example.com => $mysync->{host1} => example.com' ) ; + $mysync->{cgi} = CGI->new( 'host1=example.com' ) ; + get_options( $mysync ) ; + is( 'example.com', $mysync->{host1}, 'get_options cgi context: --host1=example.com => $mysync->{host1} => example.com' ) ; - #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - $mysync->{cgi} = CGI->new( 'simulong=' ) ; - get_options( $mysync ) ; - is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong= => $mysync->{simulong} => undef' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + $mysync->{cgi} = CGI->new( 'simulong=' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong= => $mysync->{simulong} => undef' ) ; - $mysync->{cgi} = CGI->new( 'simulong' ) ; - get_options( $mysync ) ; - is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong => $mysync->{simulong} => undef' ) ; + $mysync->{cgi} = CGI->new( 'simulong' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong => $mysync->{simulong} => undef' ) ; - $mysync->{cgi} = CGI->new( 'simulong=4' ) ; - get_options( $mysync ) ; - is( 4, $mysync->{simulong}, 'get_options cgi context: --simulong=4 => $mysync->{simulong} => 4' ) ; - is( undef, $mysync->{ folder }, 'get_options cgi context: --simulong=4 => $mysync->{ folder } => undef' ) ; - #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + $mysync->{cgi} = CGI->new( 'simulong=4' ) ; + get_options( $mysync ) ; + is( 4, $mysync->{simulong}, 'get_options cgi context: --simulong=4 => $mysync->{simulong} => 4' ) ; + is( undef, $mysync->{ folder }, 'get_options cgi context: --simulong=4 => $mysync->{ folder } => undef' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - $mysync ={} ; - $mysync->{cgi} = CGI->new( 'testslive=on' ) ; - get_options( $mysync ) ; - is( 'on', $mysync->{ testslive }, 'get_options cgi context: --testslive=on => testslive => on' ) ; - #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + $mysync ={} ; + $mysync->{cgi} = CGI->new( 'testslive=on' ) ; + get_options( $mysync ) ; + is( 'on', $mysync->{ testslive }, 'get_options cgi context: --testslive=on => testslive => on' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; $mysync ={} ; $mysync->{cgi} = CGI->new( 'log=0' ) ; get_options( $mysync ) ; is( 0, $mysync->{ log }, 'get_options cgi context: --log=0 => log => 0' ) ; #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - - - # What is this fucked up indentation? } - - note( 'Leaving tests_get_options_cgi_context()' ) ; return ; } -sub get_options_cgi +sub get_options_cgi { # In CGI context arguments are not in @ARGV but in QUERY_STRING variable (with GET). my $mysync = shift @ARG ; @@ -19736,7 +23012,9 @@ sub get_options_cgi 'f1f2=s@' => \$mysync->{ f1f2 }, 'f1f2h=s%' => \$mysync->{ f1f2h }, 'folder=s@' => \$mysync->{ folder }, + 'folderrec=s@' => \$mysync->{ folderrec }, 'testslive!' => \$mysync->{ testslive }, + 'testslive2!' => \$mysync->{ testslive2 }, 'testslive6!' => \$mysync->{ testslive6 }, 'releasecheck!' => \$mysync->{ releasecheck }, 'simulong=f' => \$mysync->{ simulong }, @@ -19745,15 +23023,38 @@ sub get_options_cgi 'subfolder2=s' => \$mysync->{ subfolder2 }, 'justfolders!' => \$mysync->{ justfolders }, 'justfoldersizes!' => \$mysync->{ justfoldersizes }, - 'delete1!' => \$mysync->{ delete1 }, - 'delete2!' => \$mysync->{ delete2 }, + + 'delete1!' => \$mysync->{ delete1 }, + 'expunge1!' => \$mysync->{ expunge1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'expunge2!' => \$mysync->{ expunge2 }, + 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, 'delete2duplicates!' => \$mysync->{ delete2duplicates }, - 'tail!' => \$mysync->{ tail }, - 'tmphash=s' => \$mysync->{ tmphash }, - 'exitwhenover=i' => \$mysync->{ exitwhenover }, - 'syncduplicates!' => \$mysync->{ syncduplicates }, - 'log!' => \$mysync->{ log }, - 'loglogfile!' => \$mysync->{ loglogfile }, + 'delete1emptyfolders' => \$mysync->{ delete1emptyfolders }, + 'delete2folders!' => \$mysync->{ delete2folders }, + + 'tail!' => \$mysync->{ tail }, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'exitonload!' => \$mysync->{ exitonload }, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'skipcrossduplicates!' => \$mysync->{ skipcrossduplicates }, + 'debugcrossduplicates!'=> \$mysync->{ debugcrossduplicates }, + 'log!' => \$mysync->{ log }, + 'loglogfile!' => \$mysync->{ loglogfile }, + 'emailreportfrom=s' => \$mysync->{ email_report_from }, + 'emailreport1!' => \$mysync->{ emailreport1 }, + 'emailreport2!' => \$mysync->{ emailreport2 }, + 'var=s@' => \$mysync->{ var }, + 'extra=s@' => \$mysync->{ extra }, + 'useheader=s@' => \$mysync->{ useheader }, + + 'minsize=i' => \$mysync->{ minsize }, + 'maxsize=i' => \$mysync->{ maxsize }, + 'maxage=f' => \$mysync->{ maxage }, + 'minage=f' => \$mysync->{ minage }, + 'search=s' => \$mysync->{ search }, + 'search1=s' => \$mysync->{ search1 }, + 'search2=s' => \$mysync->{ search2 }, # f1f2h=s% could be removed but @@ -19763,13 +23064,16 @@ sub get_options_cgi $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; - if ( ! $opt_ret ) { + if ( ! $opt_ret ) + { return ; } return $numopt ; -} +} -sub get_options_cmd + + +sub get_options_cmd { my $mysync = shift @ARG ; my @arguments = @ARG ; @@ -19787,25 +23091,23 @@ sub get_options_cmd $mysync, \@arguments, 'debug!' => \$mysync->{ debug }, - 'debuglist!' => \$debuglist, + 'debuglist!' => \$mysync->{ debuglist }, 'debugcontent!' => \$mysync->{ debugcontent }, 'debugsleep=f' => \$mysync->{ debugsleep }, - 'debugflags!' => \$debugflags, + 'debugflags!' => \$mysync->{ debugflags }, 'debugimap!' => \$mysync->{ debugimap }, 'debugimap1!' => \$mysync->{ acc1 }->{ debugimap }, 'debugimap2!' => \$mysync->{ acc2 }->{ debugimap }, - 'debugdev!' => \$debugdev, - 'debugmemory!' => \$mysync->{debugmemory}, - 'debugfolders!' => \$mysync->{debugfolders}, - 'debugssl=i' => \$mysync->{debugssl}, - 'debugcgi!' => \$debugcgi, - 'debugenv!' => \$mysync->{debugenv}, - 'debugsig!' => \$mysync->{debugsig}, - 'debuglabels!' => \$mysync->{debuglabels}, + 'debugmemory!' => \$mysync->{ debugmemory }, + 'debugfolders!' => \$mysync->{ debugfolders }, + 'debugssl=i' => \$mysync->{ debugssl }, + 'debugenv!' => \$mysync->{ debugenv }, + 'debugsig!' => \$mysync->{ debugsig }, + 'debuglabels!' => \$mysync->{ debuglabels }, - 'simulong=f' => \$mysync->{simulong}, - 'abort' => \$mysync->{abort}, - 'abortbyfile' => \$mysync->{abortbyfile}, + 'simulong=f' => \$mysync->{ simulong }, + 'abort' => \$mysync->{ abort }, + 'abortbyfile' => \$mysync->{ abortbyfile }, 'host1=s' => \$mysync->{ host1 }, 'host2=s' => \$mysync->{ host2 }, @@ -19815,119 +23117,118 @@ sub get_options_cmd 'inet6|ipv6' => \$mysync->{ inet6 }, 'user1=s' => \$mysync->{ user1 }, 'user2=s' => \$mysync->{ user2 }, - 'gmail1' => \$mysync->{gmail1}, - 'gmail2' => \$mysync->{gmail2}, - 'office1' => \$mysync->{office1}, - 'office2' => \$mysync->{office2}, - 'exchange1' => \$mysync->{exchange1}, - 'exchange2' => \$mysync->{exchange2}, - 'domino1' => \$mysync->{domino1}, - 'domino2' => \$mysync->{domino2}, + 'gmail1' => \$mysync->{ gmail1 }, + 'gmail2' => \$mysync->{ gmail2 }, + 'office1' => \$mysync->{ office1 }, + 'office2' => \$mysync->{ office2 }, + 'exchange1' => \$mysync->{ exchange1 }, + 'exchange2' => \$mysync->{ exchange2 }, + 'domino1' => \$mysync->{ domino1 }, + 'domino2' => \$mysync->{ domino2 }, 'domain1=s' => \$mysync->{ acc1 }->{ domain }, 'domain2=s' => \$mysync->{ acc2 }->{ domain }, - 'password1=s' => \$mysync->{password1}, - 'password2=s' => \$mysync->{password2}, + 'password1=s' => \$mysync->{ password1 }, + 'password2=s' => \$mysync->{ password2 }, 'passfile1=s' => \$mysync->{ passfile1 }, 'passfile2=s' => \$mysync->{ passfile2 }, 'authmd5!' => \$authmd5, 'authmd51!' => \$authmd51, 'authmd52!' => \$authmd52, - 'trylogin!' => \$mysync->{ trylogin }, - 'oauthdirect1=s' => \$mysync->{ acc1 }->{ oauthdirect }, 'oauthdirect2=s' => \$mysync->{ acc2 }->{ oauthdirect }, - 'oauthaccesstoken1=s' => \$mysync->{ acc1 }->{ oauthaccesstoken }, 'oauthaccesstoken2=s' => \$mysync->{ acc2 }->{ oauthaccesstoken }, - - 'sep1=s' => \$mysync->{ sep1 }, - 'sep2=s' => \$mysync->{ sep2 }, + 'oauthrefreshcmd1=s' => \$mysync->{ acc1 }->{ oauthrefreshcmd }, + 'oauthrefreshcmd2=s' => \$mysync->{ acc2 }->{ oauthrefreshcmd }, + 'sep1=s' => \$mysync->{ sep1 }, + 'sep2=s' => \$mysync->{ sep2 }, 'sanitize!' => \$mysync->{ sanitize }, 'folder=s@' => \$mysync->{ folder }, - 'folderrec=s' => \@folderrec, + 'folderrec=s@' => \$mysync->{ folderrec }, 'include=s' => \@include, 'exclude=s' => \@exclude, - 'noexclude' => \$mysync->{noexclude}, + 'noexclude' => \$mysync->{ noexclude }, 'folderfirst=s' => \@folderfirst, 'folderlast=s' => \@folderlast, - 'prefix1=s' => \$prefix1, - 'prefix2=s' => \$prefix2, + 'prefix1=s' => \$mysync->{ prefix1 }, + 'prefix2=s' => \$mysync->{ prefix2 }, 'subfolder1=s' => \$mysync->{ subfolder1 }, 'subfolder2=s' => \$mysync->{ subfolder2 }, 'fixslash2!' => \$mysync->{ fixslash2 }, - 'fixInboxINBOX!' => \$fixInboxINBOX, + 'fixInboxINBOX!' => \$mysync->{ fixInboxINBOX }, 'regextrans2=s@' => \$mysync->{ regextrans2 }, 'mixfolders!' => \$mixfolders, 'skipemptyfolders!' => \$mysync->{ skipemptyfolders }, 'regexmess=s' => \@regexmess, - 'noregexmess' => \$mysync->{noregexmess}, + 'noregexmess' => \$mysync->{ noregexmess }, 'skipmess=s' => \@skipmess, 'pipemess=s' => \@pipemess, - 'pipemesscheck!' => \$pipemesscheck, - 'disarmreadreceipts!' => \$disarmreadreceipts, + 'pipemesscheck!' => \$mysync->{ pipemesscheck }, + 'disarmreadreceipts!' => \$mysync->{ disarmreadreceipts }, 'regexflag=s@' => \$mysync->{ regexflag }, 'noregexflag' => \$mysync->{ noregexflag }, 'filterflags!' => \$mysync->{ filterflags }, 'filterbuggyflags!' => \$mysync->{ filterbuggyflags }, 'flagscase!' => \$mysync->{ flagscase }, - 'syncflagsaftercopy!' => \$syncflagsaftercopy, + 'syncflagsaftercopy!' => \$mysync->{ syncflagsaftercopy }, 'resyncflags!' => \$mysync->{ resyncflags }, 'synclabels!' => \$mysync->{ synclabels }, 'resynclabels!' => \$mysync->{ resynclabels }, - 'delete|delete1!' => \$mysync->{ delete1 }, + 'delete1!' => \$mysync->{ delete1 }, + 'expunge1|expunge!' => \$mysync->{ expunge1 }, 'delete2!' => \$mysync->{ delete2 }, + 'expunge2!' => \$mysync->{ expunge2 }, + 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, 'delete2duplicates!' => \$mysync->{ delete2duplicates }, - 'delete2folders!' => \$delete2folders, - 'delete2foldersonly=s' => \$delete2foldersonly, - 'delete2foldersbutnot=s' => \$delete2foldersbutnot, - 'syncinternaldates!' => \$syncinternaldates, - 'idatefromheader!' => \$idatefromheader, + 'delete1emptyfolders' => \$mysync->{ delete1emptyfolders }, + 'delete2folders!' => \$mysync->{ delete2folders }, + 'delete2foldersonly=s' => \$mysync->{ delete2foldersonly }, + 'delete2foldersbutnot=s' => \$mysync->{ delete2foldersbutnot }, + 'syncinternaldates!' => \$mysync->{ syncinternaldates }, + 'idatefromheader!' => \$mysync->{ idatefromheader }, 'syncacls!' => \$mysync->{ syncacls }, - 'maxsize=i' => \$mysync->{ maxsize }, 'appendlimit=i' => \$mysync->{ appendlimit }, 'truncmess=i' => \$mysync->{ truncmess }, - 'minsize=i' => \$minsize, - 'maxage=f' => \$maxage, - 'minage=f' => \$minage, - 'search=s' => \$search, + 'minsize=i' => \$mysync->{ minsize }, + 'maxsize=i' => \$mysync->{ maxsize }, + 'maxage=f' => \$mysync->{ maxage }, + 'minage=f' => \$mysync->{ minage }, + 'search=s' => \$mysync->{ search }, 'search1=s' => \$mysync->{ search1 }, 'search2=s' => \$mysync->{ search2 }, 'foldersizes!' => \$mysync->{ foldersizes }, 'foldersizesatend!' => \$mysync->{ foldersizesatend }, - 'dry!' => \$mysync->{dry}, - 'dry1!' => \$mysync->{dry1}, - 'expunge1|expunge!' => \$mysync->{ expunge1 }, - 'expunge2!' => \$mysync->{ expunge2 }, - 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, + 'dry!' => \$mysync->{ dry }, + 'dry1!' => \$mysync->{ dry1 }, 'subscribed' => \$subscribed, 'subscribe!' => \$subscribe, 'subscribeall|subscribe_all!' => \$subscribeall, 'justbanner!' => \$mysync->{ justbanner }, 'justfolders!'=> \$mysync->{ justfolders }, 'justfoldersizes!' => \$mysync->{ justfoldersizes }, - 'version' => \$mysync->{version}, - 'help' => \$help, - 'timeout=f' => \$mysync->{timeout}, + 'version' => \$mysync->{ version }, + 'help' => \$mysync->{ help }, + 'timeout=f' => \$mysync->{ timeout }, 'timeout1=f' => \$mysync->{ acc1 }->{timeout}, 'timeout2=f' => \$mysync->{ acc2 }->{timeout}, 'skipheader=s' => \$mysync->{ skipheader }, - 'useheader=s' => \@useheader, + 'useheader=s@' => \$mysync->{ useheader }, 'wholeheaderifneeded!' => \$wholeheaderifneeded, 'messageidnodomain!' => \$messageidnodomain, 'skipsize!' => \$skipsize, 'allowsizemismatch!' => \$allowsizemismatch, 'fastio1!' => \$mysync->{ acc1 }->{ fastio }, 'fastio2!' => \$mysync->{ acc2 }->{ fastio }, - 'sslcheck!' => \$mysync->{sslcheck}, - 'ssl1!' => \$mysync->{ssl1}, - 'ssl2!' => \$mysync->{ssl2}, - 'ssl1_ssl_version=s' => \$mysync->{ acc1 }->{sslargs}->{SSL_version}, - 'ssl2_ssl_version=s' => \$mysync->{ acc2 }->{sslargs}->{SSL_version}, - 'sslargs1=s%' => \$mysync->{ acc1 }->{sslargs}, - 'sslargs2=s%' => \$mysync->{ acc2 }->{sslargs}, - 'tls1!' => \$mysync->{tls1}, - 'tls2!' => \$mysync->{tls2}, + 'sslcheck!' => \$mysync->{ sslcheck }, + 'ssl1!' => \$mysync->{ ssl1 }, + 'ssl2!' => \$mysync->{ ssl2 }, + 'ssl1_ssl_version=s' => \$mysync->{ acc1 }->{ sslargs }->{ SSL_version }, + 'ssl2_ssl_version=s' => \$mysync->{ acc2 }->{ sslargs }->{ SSL_version }, + 'sslargs1=s%' => \$mysync->{ acc1 }->{ sslargs }, + 'sslargs2=s%' => \$mysync->{ acc2 }->{ sslargs }, + 'tls1!' => \$mysync->{ tls1 }, + 'tls2!' => \$mysync->{ tls2 }, 'uid1!' => \$uid1, 'uid2!' => \$uid2, 'authmech1=s' => \$mysync->{ acc1 }->{ authmech }, @@ -19946,96 +23247,100 @@ sub get_options_cmd 'reconnectretry1=i' => \$mysync->{ acc1 }->{ reconnectretry }, 'reconnectretry2=i' => \$mysync->{ acc2 }->{ reconnectretry }, 'tests!' => \$mysync->{ tests }, - 'testsdebug|tests_debug!' => \$mysync->{ testsdebug }, - 'testsunit=s@' => \$mysync->{testsunit}, - 'testslive!' => \$mysync->{testslive}, - 'testslive6!' => \$mysync->{testslive6}, - 'justlogin!' => \$mysync->{justlogin}, - 'justconnect!' => \$mysync->{justconnect}, + 'testsdebug!' => \$mysync->{ testsdebug }, + 'testsunit=s@' => \$mysync->{ testsunit }, + 'testslive!' => \$mysync->{ testslive }, + 'testslive2!' => \$mysync->{ testslive2 }, + 'testslive6!' => \$mysync->{ testslive6 }, + 'justlogin!' => \$mysync->{ justlogin }, + 'justconnect!' => \$mysync->{ justconnect }, 'tmpdir=s' => \$mysync->{ tmpdir }, - 'pidfile=s' => \$mysync->{pidfile}, - 'pidfilelocking!' => \$mysync->{pidfilelocking}, + 'pidfile=s' => \$mysync->{ pidfile }, + 'pidfilelocking!' => \$mysync->{ pidfilelocking }, 'sigexit=s@' => \$mysync->{ sigexit }, 'sigreconnect=s@' => \$mysync->{ sigreconnect }, 'sigignore=s@' => \$mysync->{ sigignore }, 'releasecheck!' => \$mysync->{releasecheck}, - 'modulesversion|modules_version!' => \$modulesversion, - 'usecache!' => \$usecache, - 'cacheaftercopy!' => \$cacheaftercopy, - 'debugcache!' => \$debugcache, - 'useuid!' => \$useuid, - 'addheader!' => \$mysync->{addheader}, - 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'modulesversion!' => \$mysync->{ modulesversion }, + 'usecache!' => \$mysync->{ usecache }, + 'cacheaftercopy!' => \$mysync->{ cacheaftercopy }, + 'debugcache!' => \$mysync->{ debugcache }, + 'useuid!' => \$mysync->{ useuid }, + 'addheader!' => \$mysync->{ addheader }, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'exitonload!' => \$mysync->{ exitonload }, 'checkselectable!' => \$mysync->{ checkselectable }, 'checkfoldersexist!' => \$mysync->{ checkfoldersexist }, - 'checkmessageexists!' => \$checkmessageexists, + 'checkmessageexists!' => \$mysync->{ checkmessageexists }, 'expungeaftereach!' => \$mysync->{ expungeaftereach }, - 'abletosearch!' => \$mysync->{abletosearch}, - 'abletosearch1!' => \$mysync->{abletosearch1}, - 'abletosearch2!' => \$mysync->{abletosearch2}, - 'showpasswords!' => \$mysync->{showpasswords}, + 'abletosearch!' => \$mysync->{ abletosearch }, + 'abletosearch1!' => \$mysync->{ abletosearch1 }, + 'abletosearch2!' => \$mysync->{ abletosearch2 }, + 'showpasswords!' => \$mysync->{ showpasswords }, 'maxlinelength=i' => \$maxlinelength, - 'maxlinelengthcmd=s' => \$maxlinelengthcmd, - 'minmaxlinelength=i' => \$minmaxlinelength, - 'debugmaxlinelength!' => \$debugmaxlinelength, - 'fixcolonbug!' => \$fixcolonbug, - 'create_folder_old!' => \$create_folder_old, - 'maxmessagespersecond=f' => \$mysync->{maxmessagespersecond}, - 'maxbytespersecond=i' => \$mysync->{maxbytespersecond}, - 'maxbytesafter=i' => \$mysync->{maxbytesafter}, - 'maxsleep=f' => \$mysync->{maxsleep}, - 'skipcrossduplicates!' => \$skipcrossduplicates, - 'debugcrossduplicates!' => \$debugcrossduplicates, - 'log!' => \$mysync->{log}, - 'tail!' => \$mysync->{tail}, - 'logfile=s' => \$mysync->{logfile}, - 'logdir=s' => \$mysync->{logdir}, - 'errorsmax=i' => \$mysync->{errorsmax}, + 'fixcolonbug!' => \$mysync->{ fixcolonbug }, + 'create_folder_old!' => \$mysync->{ create_folder_old }, + 'maxmessagespersecond=f' => \$mysync->{ maxmessagespersecond }, + 'maxbytespersecond=i' => \$mysync->{ maxbytespersecond }, + 'maxbytesafter=i' => \$mysync->{ maxbytesafter }, + 'maxsleep=f' => \$mysync->{ maxsleep }, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'skipcrossduplicates!' => \$mysync->{ skipcrossduplicates }, + 'debugcrossduplicates!' => \$mysync->{ debugcrossduplicates }, + 'log!' => \$mysync->{ log }, + 'tail!' => \$mysync->{ tail }, + 'logfile=s' => \$mysync->{ logfile }, + 'logdir=s' => \$mysync->{ logdir }, + 'errorsmax=i' => \$mysync->{ errorsmax }, 'errorsdump!' => \$mysync->{ errorsdump }, - 'fetch_hash_set=s' => \$fetch_hash_set, - 'automap!' => \$mysync->{automap}, - 'justautomap!' => \$mysync->{justautomap}, - 'id!' => \$mysync->{id}, - 'f1f2=s@' => \$mysync->{f1f2}, - 'nof1f2' => \$mysync->{nof1f2}, - 'f1f2h=s%' => \$mysync->{f1f2h}, - 'justfolderlists!' => \$mysync->{justfolderlists}, - 'delete1emptyfolders' => \$mysync->{delete1emptyfolders}, - 'checknoabletosearch!' => \$mysync->{checknoabletosearch}, - 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'fetch_hash_set=s' => \$mysync->{ fetch_hash_set }, + 'automap!' => \$mysync->{ automap }, + 'justautomap!' => \$mysync->{ justautomap }, + 'id!' => \$mysync->{ id }, + 'f1f2=s@' => \$mysync->{ f1f2 }, + 'nof1f2' => \$mysync->{ nof1f2 }, + 'f1f2h=s%' => \$mysync->{ f1f2h }, + 'justfolderlists!' => \$mysync->{ justfolderlists }, + 'checknoabletosearch!' => \$mysync->{ checknoabletosearch }, 'dockercontext!' => \$mysync->{ dockercontext }, - - + 'emailreportfrom=s' => \$mysync->{ email_report_from }, + 'emailreport1!' => \$mysync->{ emailreport1 }, + 'emailreport2!' => \$mysync->{ emailreport2 }, + 'var=s@' => \$mysync->{ var }, + 'memorystress!' => \$mysync->{ memorystress }, + 'extra=s@' => \$mysync->{ extra }, ) ; - #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; + my $numopt_after = scalar @arguments ; - #myprint( "get options: [$opt_ret][$numopt][$numopt_after]\n" ) ; + $mysync->{ debug } and output( $mysync, "get options: opt_ret[$opt_ret] numopt[$numopt] numopt_after[$numopt_after]\n" ) ; # The $arguments[0] test is just because parallel adds "" when it is # used with {=7=} in sync_parallel_unix.sh - if ( $numopt_after and $arguments[0] ) { + if ( $numopt_after and $arguments[0] ) + { myprint( - "Found ", scalar( @arguments ), " extra arguments : [@arguments]\n", + "Found $numopt_after supplementary arguments: [@arguments]\n", "It usually means a quoting issue in the command line ", - "or some misspelling options.\n", + "or some misspelled or unknown options.\n", ) ; return ; } + if ( ! $opt_ret ) { return ; } return $numopt ; -} +} -sub tests_get_options + +sub tests_get_options { note( 'Entering tests_get_options()' ) ; # CAVEAT: still setting global variables, be careful - # with tests, the context increases! $debug stays on for example. + # with tests, the context increases! $debug used to stay on for example. # API: # * input arguments: two ways, command line or CGI # * the program arguments @@ -20066,27 +23371,28 @@ sub tests_get_options is( undef, get_options( $mysync, qw( --noexist --version ) ), 'get_options: --debug --noexist --version => undef' ) ; is( 1, get_options( $mysync, qw( --version ) ), 'get_options: --version => 1' ) ; - is( undef, get_options( $mysync, qw( extra ) ), 'get_options: extra => undef' ) ; - is( undef, get_options( $mysync, qw( extra1 --version extra2 ) ), 'get_options: extra1 --version extra2 => undef' ) ; + is( undef, get_options( $mysync, qw( kaka ) ), 'get_options: kaka => undef' ) ; + is( undef, get_options( $mysync, qw( kaka1 --version kaka2 ) ), 'get_options: extra1 --version extra2 => undef' ) ; $mysync = { } ; - is( 2, get_options( $mysync, qw( --host1 HOST_01) ), 'get_options: --host1 HOST_01 => 1' ) ; - is( 'HOST_01', $mysync->{ host1 }, 'get_options: --host1 HOST_01 => HOST_01' ) ; + is( 2, get_options( $mysync, qw( --host1 HOST_01) ), 'get_options: --host1 HOST_01 => 2' ) ; + is( 'HOST_01', $mysync->{ host1 }, 'get_options: --host1 HOST_01 => host1 = HOST_01' ) ; #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; note( 'Leaving tests_get_options()' ) ; return ; -} +} -sub get_options +sub get_options { my $mysync = shift @ARG ; my @arguments = @ARG ; #myprint( "1 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; my $ret ; - if ( under_cgi_context( ) ) { + if ( under_cgi_context( ) ) + { # CGI context $ret = get_options_cgi( $mysync, @arguments ) ; }else{ @@ -20094,7 +23400,7 @@ sub get_options $ret = get_options_cmd( $mysync, @arguments ) ; } ; #myprint( "2 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; - + foreach my $key ( sort keys %{ $mysync } ) { if ( ! defined $mysync->{$key} ) { delete $mysync->{$key} ; @@ -20109,42 +23415,40 @@ sub get_options } -sub tests_infos +sub tests_infos { note( 'Entering tests_infos()' ) ; note( "OSNAME=$OSNAME" ) ; - note( "hostname=". hostname() ) ; + note( "hostname=". hostname( ) ) ; note( "cwd=" . getcwd( ) ) ; note( "PROGRAM_NAME=$PROGRAM_NAME" ) ; - my $stat = stat("$PROGRAM_NAME") ; - my $perms = sprintf( "%04o\n", $stat->mode & oct($PERMISSION_FILTER) ) ; + my $stat = stat( "$PROGRAM_NAME" ) ; + my $perms = sprintf( "%04o\n", $stat->mode & oct( $PERMISSION_FILTER ) ) ; note( "permissions=$perms" ) ; note( "PROCESS_ID=$PROCESS_ID" ) ; note( "REAL_USER_ID=$REAL_USER_ID" ) ; note( "EFFECTIVE_USER_ID=$EFFECTIVE_USER_ID" ) ; note( "context: " . imapsync_context( $sync ) ) ; - note( "memory_consumption: " . memory_consumption() . " bytes aka " . bytes_display_string_dec( memory_consumption() ) ) ; - cpu_number - note( "cpu_number: " . cpu_number() ) ; - note( $sync->{rcs} ) ; - + note( "memory_consumption_of_myself: " . memory_consumption_of_myself( ) . " bytes aka " . bytes_display_string_dec( memory_consumption_of_myself( ) ) ) ; + note( "cpu_number: " . cpu_number( ) ) ; + note( $sync->{ rcs } ) ; note( 'Leaving tests_infos()' ) ; return ; -} +} sub condition_to_leave_after_tests { - my $mysync = shift ; - if ( $mysync->{ testslive } or $mysync->{ testslive6 } ) + my $mysync = shift @ARG ; + if ( $mysync->{ testslive } or $mysync->{ testslive6 } or $mysync->{ testslive2 } ) { return 0 ; } - if ( $mysync->{ tests } - or $mysync->{ testsdebug } - or $mysync->{ testsunit } + if ( $mysync->{ tests } + or $mysync->{ testsdebug } + or $mysync->{ testsunit } ) { return 1 ; @@ -20154,7 +23458,7 @@ sub condition_to_leave_after_tests sub testunitsession { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync ) { return ; } if ( ! $mysync->{ testsunit } ) { return ; } @@ -20214,7 +23518,8 @@ sub report_failures my $counter = 1 ; my $report = q{} ; foreach my $details ( @details ) { - if ( ! $details->{ 'ok' } ) { + if ( ! $details->{ 'ok' } ) + { my $name = $details->{ 'name' } || 'NONAME' ; $report .= "nb $counter - $name\n" ; } @@ -20228,11 +23533,25 @@ sub tests_true { note( 'Entering tests_true()' ) ; - is( 1, 1, 'true: 1 is 1' ) ; + is( 1, 1, 'true: 1 is 1' ) ; + is( 'A', 'A', 'true: A is A' ) ; note( 'Leaving tests_true()' ) ; return ; } + +sub tests_always_fail +{ + note( 'Entering tests_always_fail()' ) ; + + is( 0, 1, 'always_fail: 0 is 1' ) ; + isnt( 'A', 'A', 'always_fail: A is A' ) ; + + note( 'Leaving tests_always_fail()' ) ; + return ; +} + + sub tests_testsunit { note( 'Entering tests_testunit()' ) ; @@ -20250,21 +23569,25 @@ sub testsunit { my @functions = @ARG ; - if ( ! @functions ) { # + if ( ! @functions ) + { # myprint( "testsunit warning: no argument given\n" ) ; return ; } foreach my $function ( @functions ) { - if ( ! $function ) { + if ( ! $function ) + { myprint( "testsunit warning: argument is empty\n" ) ; next ; } - if ( ! exists &$function ) { + if ( ! exists &$function ) + { myprint( "testsunit warning: function $function does not exist\n" ) ; next ; } - if ( ! defined &$function ) { + if ( ! defined &$function ) + { myprint( "testsunit warning: function $function is not defined\n" ) ; next ; } @@ -20278,9 +23601,10 @@ sub testsdebug { # Now a little obsolete since there is # imapsync ... --testsunit "anyfunction" - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync->{ testsdebug } ) { return ; } - SKIP: { + SKIP: + { if ( ! $mysync->{ testsdebug } ) { skip 'No test in normal run' ; } @@ -20293,11 +23617,35 @@ sub testsdebug #tests_kill_zero( ) ; #tests_connect_socket( ) ; #tests_probe_imapssl( ) ; - #tests_cpu_number( ) ; #tests_mailimapclient_connect( ) ; - tests_loadavg( ) ; #tests_always_fail( ) ; - + #tests_localtimez( ) ; + #tests_year_month_day_hour_min_sec_ms( ) ; + #tests_date_rfc822( ) ; + #tests_email_report_message_id( ) ; + #tests_all_pids( ) ; + #tests_memory_consumption_of_myself( ) ; + #tests_ram_memory_info( ) ; + #tests_memory_consumption_of_all_pids( ) ; + #tests_memory_consumption_all_pids_percent( ) ; + + #tests_memory_stress( ) ; + + #tests_memory_consumption_of_pids_win32( ) ; + + #tests_load_and_delay( ) ; + #tests_cpu_number( ) ; + #tests_loadavg( ) ; + #tests_load_per_cpu( ) ; + #tests_all_pids( ) ; + #tests_remove_qq( ) ; + #tests_remove_not_num( ) ; + + #tests_compress_starttls( ) ; + #tests_diskspace_available( ) ; + tests_tmpdir( ) ; + tests_infos( ) ; + note( 'Leaving testsdebug()' ) ; done_testing( ) ; } @@ -20305,14 +23653,15 @@ sub testsdebug } -sub tests +sub tests { - my $mysync = shift ; + my $mysync = shift @ARG ; if ( ! $mysync->{ tests } ) { return ; } SKIP: { skip 'No test in normal run' if ( ! $mysync->{ tests } ) ; note( 'Entering tests()' ) ; + printenv( $mysync ) ; tests_folder_routines( ) ; tests_compare_lists( ) ; tests_regexmess( ) ; @@ -20327,7 +23676,7 @@ sub tests tests_good_date( ) ; tests_max( ) ; tests_remove_not_num(); - tests_memory_consumption( ) ; + tests_memory_consumption_of_myself( ) ; tests_is_a_release_number(); tests_imapsync_basename(); tests_list_keys_in_2_not_in_1(); @@ -20375,7 +23724,7 @@ sub tests tests_quota_extract_storage_limit_in_bytes( ) ; tests_quota_extract_storage_current_in_bytes( ) ; tests_guess_special( ) ; - tests_do_valid_directory( ) ; + tests_make_valid_directory( ) ; tests_delete1emptyfolders( ) ; tests_message_for_host2( ) ; tests_length_ref( ) ; @@ -20388,7 +23737,6 @@ sub tests tests_match( ) ; tests_get_options( ) ; tests_get_options_cgi_context( ) ; - tests_rand32( ) ; tests_hashsynclocal( ) ; tests_hashsync( ) ; tests_output( ) ; @@ -20468,7 +23816,6 @@ sub tests tests_eta( ) ; tests_timesince( ) ; tests_timenext( ) ; - tests_foldersize( ) ; tests_imapsync_context( ) ; tests_abort( ) ; tests_probe_imapssl( ) ; @@ -20484,7 +23831,9 @@ sub tests tests_total_bytes_max_reached( ) ; tests_header_construct( ) ; tests_remove_doublequotes_if_any( ) ; - tests_login_imap( ) ; + tests_login_imap_ssl_993( ) ; + tests_login_imap_starttls( ) ; + tests_login_imap_notls_nossl( ) ; tests_login_imap_oauth( ) ; tests_skipmess_neg( ) ; tests_localtimez( ) ; @@ -20498,9 +23847,52 @@ sub tests tests_comment_of_error_type( ) ; tests_debugcontent( ) ; tests_compress_ssl( ) ; + tests_compress_starttls( ) ; tests_compress( ) ; - tests_get_options_extra( ) ; + tests_get_options_extra_from_file( ) ; tests_get_options_from_string( ) ; + tests_email_report_message_id( ) ; + tests_year_month_day_hour_min_sec_ms( ) ; + tests_fractional_of_floor( ) ; + tests_date_rfc822( ) ; + tests_email_report_from( ) ; + tests_email_report_to( ) ; + tests_email_report_body_base( ) ; + tests_email_report( ) ; + tests_setlogdir( ) ; + tests_logfilesuffix( ) ; + tests_cgienvcontext( ) ; + tests_usecache_and_skipcrossduplicates( ) ; + tests_loglogfile( ) ; + tests_heavy_load_reached( ) ; + tests_heavy_load_percent_threshold( ) ; + tests_pctmem_available( ) ; + tests_filterbuggyflags( ) ; + tests_heavy_load_reached_by_memory( ) ; + tests_heavy_load_reached_by_cpu( ) ; + tests_load_per_cpu( ) ; + tests_memory_consumption_surface( ) ; + tests_add( ) ; + tests_all_pids( ) ; + tests_memory_consumption_of_all_pids( ) ; + tests_remove_qq( ) ; + tests_memory_consumption_all_pids_percent( ) ; + tests_ram_memory_info( ) ; + tests_firstline_or_string( ) ; + tests_oauth2_bearer_string( ) ; + tests_diskspace_available( ) ; + tests_generate_a_random_string( ) ; + tests_filter_forbidden_characters_cgi_context( ) ; + tests_trigger_justconnect1( ) ; + tests_trigger_justconnect2( ) ; + tests_tmpdir( ) ; + tests_cgidir( ) ; + tests_separate_host_port_folder( ) ; + tests_host1_port1_folderrec_from_host1( ) ; + tests_get_options_extra_from_param( ) ; + tests_setlog( ) ; + tests_delete_folders_in_2_not_in_1( ) ; + tests_subfolder2_with_delete2folders( ) ; tests_infos( ) ; #tests_resolv( ) ; @@ -20511,21 +23903,23 @@ sub tests #tests_kill_zero( ) ; #tests_always_fail( ) ; - done_testing( 1860 ) ; + + done_testing( 2189 ) ; note( 'Leaving tests()' ) ; } return ; } + sub tests_template { note( 'Entering tests_template()' ) ; - is( undef, template( ), 'template: no args => undef' ) ; + is( undef, template( ), 'tests_template: no args => undef' ) ; my $mysync = { } ; - is( undef, template( $mysync ), 'template: undef => undef' ) ; - is_deeply( {}, {}, 'template: a hash is a hash' ) ; - is_deeply( [], [], 'template: an array is an array' ) ; + is( undef, template( $mysync ), 'tests_template: { } => undef' ) ; + is_deeply( {}, {}, 'tests_template: a hash is a hash' ) ; + is_deeply( [], [], 'tests_template: an array is an array' ) ; note( 'Leaving tests_template()' ) ; return ; @@ -20534,6 +23928,7 @@ sub tests_template sub template { my $mysync = shift @ARG ; - + return ; } + diff --git a/data/Dockerfiles/dovecot/imapsync_runner.pl b/data/Dockerfiles/dovecot/imapsync_runner.pl deleted file mode 100644 index 1030603ce..000000000 --- a/data/Dockerfiles/dovecot/imapsync_runner.pl +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/perl - -use DBI; -use LockFile::Simple qw(lock trylock unlock); -use Proc::ProcessTable; -use Data::Dumper qw(Dumper); -use IPC::Run 'run'; -use File::Temp; -use Try::Tiny; -use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT); - -sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s }; -my $t = Proc::ProcessTable->new; -my $imapsync_running = grep { $_->{cmndline} =~ /imapsync\s/i } @{$t->table}; -if ($imapsync_running ge 1) -{ - print "imapsync is active, exiting..."; - exit; -} - -sub qqw($) { - my @params = (); - my @values = split(/(?=--)/, $_[0]); - foreach my $val (@values) { - my @tmpparam = split(/ /, $val, 2); - foreach my $tmpval (@tmpparam) { - if ($tmpval ne '') { - push @params, $tmpval; - } - } - } - foreach my $val (@params) { - $val=trim($val); - } - return @params; -} - -$run_dir="/tmp"; -$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock'; -$lock_file = $run_dir . "/imapsync_busy"; -$lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1); -$lockmgr->lock($lock_file) || die "can't lock ${lock_file}"; -$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, { - mysql_auto_reconnect => 1, - mysql_enable_utf8mb4 => 1 -}); -$dbh->do("UPDATE imapsync SET is_running = 0"); - -sub sig_handler { - # Send die to force exception in "run" - die "sig_handler received signal, preparing to exit...\n"; -}; - -open my $file, '<', "/etc/sogo/sieve.creds"; -my $creds = <$file>; -close $file; -my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, - user1, - user2, - host1, - authmech1, - password1, - exclude, - port1, - enc1, - delete2duplicates, - maxage, - subfolder2, - delete1, - delete2, - automap, - skipcrossduplicates, - maxbytespersecond, - custom_params, - subscribeall, - timeout1, - timeout2, - dry - FROM imapsync - WHERE active = 1 - AND is_running = 0 - AND ( - UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 - OR - last_run IS NULL) - ORDER BY last_run"); - -$sth->execute(); -my $row; - -while ($row = $sth->fetchrow_arrayref()) { - - $id = @$row[0]; - $user1 = @$row[1]; - $user2 = @$row[2]; - $host1 = @$row[3]; - $authmech1 = @$row[4]; - $password1 = @$row[5]; - $exclude = @$row[6]; - $port1 = @$row[7]; - $enc1 = @$row[8]; - $delete2duplicates = @$row[9]; - $maxage = @$row[10]; - $subfolder2 = @$row[11]; - $delete1 = @$row[12]; - $delete2 = @$row[13]; - $automap = @$row[14]; - $skipcrossduplicates = @$row[15]; - $maxbytespersecond = @$row[16]; - $custom_params = @$row[17]; - $subscribeall = @$row[18]; - $timeout1 = @$row[19]; - $timeout2 = @$row[20]; - $dry = @$row[21]; - - if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } - - my $template = $run_dir . '/imapsync.XXXXXXX'; - my $passfile1 = File::Temp->new(TEMPLATE => $template); - my $passfile2 = File::Temp->new(TEMPLATE => $template); - - binmode( $passfile1, ":utf8" ); - - print $passfile1 "$password1\n"; - print $passfile2 trim($master_pass) . "\n"; - - my @custom_params_a = qqw($custom_params); - my $custom_params_ref = \@custom_params_a; - - my $generated_cmds = [ "/usr/local/bin/imapsync", - "--tmpdir", "/tmp", - "--nofoldersizes", - "--addheader", - ($timeout1 le "0" ? () : ('--timeout1', $timeout1)), - ($timeout2 le "0" ? () : ('--timeout2', $timeout2)), - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), - ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)), - ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - ($subscribeall ne "1" ? () : ('--subscribeall')), - ($delete1 ne "1" ? () : ('--delete')), - ($delete2 ne "1" ? () : ('--delete2')), - ($automap ne "1" ? () : ('--automap')), - ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')), - (!defined($enc1) ? () : ($enc1)), - "--host1", $host1, - "--user1", $user1, - "--passfile1", $passfile1->filename, - "--port1", $port1, - "--host2", "localhost", - "--user2", $user2 . '*' . trim($master_user), - "--passfile2", $passfile2->filename, - ($dry eq "1" ? ('--dry') : ()), - '--no-modulesversion', - '--noreleasecheck']; - - try { - $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?"); - $is_running->bind_param( 1, ${id} ); - $is_running->execute(); - - run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout; - - # check exit code and status - ($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/); - - $success = 0; - if (defined $exit_code && $exit_code == 0) { - $success = 1; - } - - $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?"); - $update->bind_param( 1, ${stdout} ); - $update->bind_param( 2, ${success} ); - $update->bind_param( 3, ${exit_status} ); - $update->bind_param( 4, ${id} ); - $update->execute(); - } catch { - $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?"); - $update->bind_param( 1, ${id} ); - $update->execute(); - } finally { - $update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?"); - $update->bind_param( 1, ${id} ); - $update->execute(); - }; - - -} - -$sth->finish(); -$dbh->disconnect(); - -$lockmgr->unlock($lock_file); diff --git a/data/Dockerfiles/dovecot/imapsync_runner.py b/data/Dockerfiles/dovecot/imapsync_runner.py new file mode 100755 index 000000000..09fa1eda6 --- /dev/null +++ b/data/Dockerfiles/dovecot/imapsync_runner.py @@ -0,0 +1,331 @@ +#!/usr/bin/python3 + +import fcntl +import json +import os +import re +import signal +import subprocess +import sys +import tempfile +import time +import MySQLdb +import MySQLdb.cursors + +run_dir = '/tmp' +# Max size of imapsync output stored in returned_text (the tail holds the summary) +max_output = 16 * 1024 * 1024 +lock_file = run_dir + '/imapsync_busy.lock' +# An OAuth run needs a token valid this long at start (half the lifetime for short-lived tokens) +token_min_valid = 900 +# Refreshed tokens are written to the token files of running jobs this often, imapsync re-reads them on reconnect +token_sync_interval = 60 + +# Next due job: higher prio first, never-run jobs first, then oldest run +next_sql = """SELECT i.id, i.user1, i.user2, i.password1, i.exclude, i.delete2duplicates, i.maxage, + i.subfolder2, i.delete1, i.delete2, i.automap, i.skipcrossduplicates, i.maxbytespersecond, i.custom_params, + i.subscribeall, i.timeout1, i.timeout2, i.dry, i.source_id, + s.host1, s.port1, s.enc1, s.auth_type, s.oauth_flow + FROM imapsync i JOIN imapsync_source s ON i.source_id = s.id + WHERE i.active = 1 + AND s.active = 1 + AND i.is_running = 0 + AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60 OR i.last_run IS NULL) + {skip} + ORDER BY i.prio DESC, i.last_run IS NOT NULL, i.last_run ASC, i.id ASC + LIMIT 1""" +# Atomic claim, a job is only started when this flips is_running 0 -> 1 +claim_sql = """UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL + WHERE id = %s AND is_running = 0 AND active = 1""" +# Access token: client_credentials share the source token, authorization_code use the token of +# source + local mailbox (user2) + remote user (user1). issued/error describe the last refresh. +source_token_sql = """SELECT oauth_access_token AS access_token, oauth_token_expires AS token_expires, + UNIX_TIMESTAMP(oauth_last_refresh_attempt) AS issued, oauth_last_refresh_error AS error + FROM imapsync_source WHERE id = %s""" +user_token_sql = """SELECT access_token, token_expires, UNIX_TIMESTAMP(oauth_last_refresh_attempt) AS issued, + last_refresh_error AS error FROM imapsync_source_oauth_token + WHERE source_id = %s AND owner = %s AND username = %s""" + +conn = None +# Popen => job of running workers +running = {} +# Jobs released without a run (token not ready), skipped for the rest of this runner round +deferred = set() +stopping = False + +def num(value): + try: + return int(value) + except (TypeError, ValueError): + return 0 + +def unlink(files): + for path in files: + try: + os.unlink(path) + except OSError: + pass + +def imapsync_running(): + for pid in filter(str.isdigit, os.listdir('/proc')): + try: + with open('/proc/' + pid + '/cmdline', 'rb') as f: + cmdline = f.read().replace(b'\0', b' ') + except OSError: + continue + if re.search(rb'imapsync\s', cmdline, re.I): + return True + return False + +def connect(): + return MySQLdb.connect(unix_socket='/var/run/mysqld/mysqld.sock', user=os.environ['DBUSER'], + passwd=os.environ['DBPASS'], db=os.environ['DBNAME'], charset='utf8mb4', autocommit=True) + +# Reconnect when the server dropped the connection (syncs can outlast wait_timeout) +def query(sql, args=None): + global conn + try: + conn.ping() + except MySQLdb.Error: + conn = connect() + cur = conn.cursor(MySQLdb.cursors.DictCursor) + cur.execute(sql, args) + return cur + +# Store a result and release the job +def finish_job(job_id, text, success, exit_status): + try: + query("UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s, last_run = NOW(), is_running = 0 WHERE id = %s", + (text, success, exit_status, job_id)) + except MySQLdb.Error: + query("UPDATE imapsync SET success = 0, last_run = NOW(), is_running = 0 WHERE id = %s", (job_id,)) + +# Release a job without a run; last_run stays, so the next runner round retries it +def defer_job(job_id, text, exit_status): + query("UPDATE imapsync SET returned_text = %s, success = 0, exit_status = %s, is_running = 0 WHERE id = %s", + (text, exit_status, job_id)) + deferred.add(job_id) + +def oauth_token(j): + if j['oauth_flow'] == 'authorization_code': + return query(user_token_sql, (j['source_id'], j['user2'], j['user1'])).fetchone() or {} + return query(source_token_sql, (j['source_id'],)).fetchone() or {} + +# Valid for token_min_valid, or half the lifetime of a short-lived token (known when its last refresh succeeded) +def token_ready(row): + token, expires, issued = row.get('access_token'), row.get('token_expires'), row.get('issued') + if not token or expires is None: + return False + need = token_min_valid + if issued and not row.get('error') and expires > int(issued): + need = min(need, (expires - int(issued)) // 2) + return expires - time.time() >= need + +# Write a refreshed token over the token file of a running job (atomic, same path) +def sync_token_file(oauth): + token = oauth_token(oauth['job']).get('access_token') + if not token or token == oauth['token']: + return + fd, tmp = tempfile.mkstemp(prefix='imapsync.', dir=run_dir) + try: + with os.fdopen(fd, 'w', encoding='utf-8') as f: + f.write(token + '\n') + os.replace(tmp, oauth['file']) + oauth['token'] = token + except OSError: + unlink([tmp]) + +# Max concurrent imapsync processes and global per-process bandwidth cap in bytes/s (0 = off), +# re-read before every claim round so changes apply to long runs +def read_settings(): + settings = {r['name']: r['value'] for r in query("SELECT name, value FROM imapsync_settings")} + return max(num(settings.get('max_parallel')), 1), max(num(settings.get('max_bytes_per_second')), 0) + +# Build temp files and argv for a claimed job. Returns None when the job was finished without running imapsync. +def prepare_job(j, global_bw, files): + def tmpfile(content): + fd, path = tempfile.mkstemp(prefix='imapsync.', dir=run_dir) + files.append(path) + with os.fdopen(fd, 'w', encoding='utf-8') as f: + f.write(content) + return path + + auth_type = j['auth_type'] or 'PLAIN' + oauth = None + if auth_type == 'XOAUTH2': + row = oauth_token(j) + if not token_ready(row): + defer_job(j['id'], "OAuth access token for the configured source is missing or expires too soon; refresh cron will retry.", + 'OAUTH_TOKEN_NOT_READY') + return None + oauth = {'job': j, 'token': row['access_token'], 'file': tmpfile(row['access_token'] + '\n')} + auth_args = ['--authmech1', 'XOAUTH2', '--oauthaccesstoken1', oauth['file']] + else: + auth_args = ['--passfile1', tmpfile(j['password1'] + '\n')] + # imapsync auto-picks PLAIN, only pass --authmech1 for LOGIN/CRAM-MD5 + if auth_type != 'PLAIN': + auth_args += ['--authmech1', auth_type] + + # Custom params: JSON array of {o,v}, validated by the backend on write. One argv token per pair, + # "--opt" or "--opt=value" (value glued to its option, no shell) + custom_params = [] + try: + pairs = json.loads(j['custom_params'] or '[]') + except ValueError: + pairs = [] + for p in pairs if isinstance(pairs, list) else []: + if not isinstance(p, dict) or not isinstance(p.get('o'), str) or not re.fullmatch(r'[a-z0-9]+', p['o']): + continue + value = '' if p.get('v') is None else str(p['v']) + custom_params.append('--' + p['o'] if value == '' else '--' + p['o'] + '=' + value) + + # Effective per-process bandwidth cap (bytes/s): per-job value, capped by the global limit + eff_bw = num(j['maxbytespersecond']) + if global_bw > 0 and (eff_bw <= 0 or eff_bw > global_bw): + eff_bw = global_bw + + # --nolog: the output is stored in returned_text, imapsync's own log dir would be the (read-only) cwd + cmd = ['/usr/local/bin/imapsync', '--tmpdir', '/tmp', '--nolog', '--nofoldersizes', '--addheader'] + if num(j['timeout1']) > 0: + cmd += ['--timeout1', str(j['timeout1'])] + if num(j['timeout2']) > 0: + cmd += ['--timeout2', str(j['timeout2'])] + if j['exclude']: + cmd += ['--exclude', j['exclude']] + if j['subfolder2']: + cmd += ['--subfolder2', j['subfolder2']] + if num(j['maxage']) != 0: + cmd += ['--maxage', str(j['maxage'])] + if eff_bw > 0: + cmd += ['--maxbytespersecond', str(eff_bw)] + for col, opt in (('delete2duplicates', '--delete2duplicates'), ('subscribeall', '--subscribeall'), + ('delete1', '--delete1'), ('delete2', '--delete2'), ('automap', '--automap'), + ('skipcrossduplicates', '--skipcrossduplicates')): + if j[col] == 1: + cmd.append(opt) + enc1 = {'TLS': '--tls1', 'SSL': '--ssl1'}.get(j['enc1']) + if enc1: + cmd.append(enc1) + cmd += ['--host1', j['host1'], '--user1', j['user1']] + auth_args + ['--port1', str(j['port1'])] + cmd += ['--host2', 'localhost', '--user2', j['user2'] + '*' + master_user, '--passfile2', tmpfile(master_pass + '\n')] + if j['dry'] == 1: + cmd.append('--dry') + cmd += ['--no-modulesversion', '--noreleasecheck'] + custom_params + + return {'id': j['id'], 'files': files, 'outfile': tmpfile(''), 'cmd': cmd, 'oauth': oauth} + +# Worker: imapsync with output to a file, no inherited fds (DB socket, lock) +def start_job(job): + with open(job['outfile'], 'wb') as out: + return subprocess.Popen(job['cmd'], stdin=subprocess.DEVNULL, stdout=out, stderr=subprocess.STDOUT) + +# Store the result of a finished worker and remove its temp files +def reap_job(job): + stdout = b'' + try: + with open(job['outfile'], 'rb') as f: + size = os.fstat(f.fileno()).st_size + if size > max_output: + f.seek(size - max_output) + stdout = b'[output truncated, last %d bytes]\n' % max_output + stdout += f.read() + except OSError: + pass + unlink(job['files']) + text = stdout.decode('utf-8', 'replace') + # Last match: the echoed command line at the top may contain user controlled values + m = re.findall(r'Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)', text) + exit_code, exit_status = m[-1] if m else (None, None) + if exit_code is None: + text = 'Could not start or finish imapsync\n' + text + finish_job(job['id'], text, 1 if exit_code is not None and int(exit_code) == 0 else 0, exit_status) + +# Stop claiming new jobs, pass the signal on to the running imapsync processes +def stop(signum, frame): + global stopping + stopping = True + for proc in list(running): + proc.send_signal(signal.SIGTERM) + +# Released by the kernel when the process ends, a crashed run leaves no stale lock +lock_fd = os.open(lock_file, os.O_RDWR | os.O_CREAT, 0o600) +try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) +except OSError: + sys.exit("can't lock " + lock_file) + +if imapsync_running(): + print('imapsync is active, exiting...') + sys.exit(0) + +for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT): + signal.signal(sig, stop) + +conn = connect() +# Stale flags from crashed runs, safe only here: under the lock and with no imapsync running +query("UPDATE imapsync SET is_running = 0") + +with open('/etc/sogo/sieve.creds') as f: + master_user, master_pass = [s.strip() for s in f.readline().split(':', 1)] + +# Worker pool: keep up to max_parallel workers busy, claim the next due job whenever a slot is free +# (after every worker exit and at least every 10 seconds while workers run) +last_token_sync = time.monotonic() +try: + while True: + for proc, job in list(running.items()): + if proc.poll() is not None: + del running[proc] + reap_job(job) + + if time.monotonic() - last_token_sync >= token_sync_interval: + last_token_sync = time.monotonic() + for job in running.values(): + if job['oauth']: + try: + sync_token_file(job['oauth']) + except MySQLdb.Error as e: + print('imapsync job %d: %s' % (job['id'], e), file=sys.stderr) + + max_parallel, global_bw = read_settings() + # Deferred and finished jobs drop out of next_sql, only failed claims are capped + failed_claims = 0 + while not stopping and len(running) < max_parallel and failed_claims < 100: + skip = ('AND i.id NOT IN (%s)' % ', '.join(['%s'] * len(deferred))) if deferred else '' + j = query(next_sql.format(skip=skip), tuple(deferred) or None).fetchone() + if not j: + break + if query(claim_sql, (j['id'],)).rowcount != 1: + failed_claims += 1 + continue + + files = [] + try: + job = prepare_job(j, global_bw, files) + except Exception as e: + print('imapsync job %d: %s' % (j['id'], e), file=sys.stderr) + finish_job(j['id'], 'Could not start or finish imapsync', 0, None) + job = None + if not job: + unlink(files) + continue + try: + running[start_job(job)] = job + except OSError as e: + # Release the claim, the job is picked up again later + print('imapsync job %d: %s' % (j['id'], e), file=sys.stderr) + unlink(files) + query("UPDATE imapsync SET is_running = 0 WHERE id = %s", (j['id'],)) + break + + if not running: + break + # A worker exit ends the wait early + for _ in range(50): + if any(proc.poll() is not None for proc in running): + break + time.sleep(0.2) +finally: + # Never leave password/token files behind + for job in running.values(): + unlink(job['files']) diff --git a/data/conf/phpfpm/crons/imapsync-oauth-refresh.php b/data/conf/phpfpm/crons/imapsync-oauth-refresh.php new file mode 100644 index 000000000..a90088963 --- /dev/null +++ b/data/conf/phpfpm/crons/imapsync-oauth-refresh.php @@ -0,0 +1,73 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} catch (PDOException $e) { + fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n"); + exit(1); +} + +$redis = new Redis(); +try { + if (!empty(getenv('REDIS_SLAVEOF_IP'))) { + $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} catch (Exception $e) { + fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n"); + exit(1); +} + +function logMsg($priority, $message, $task = "imapsync OAuth refresh") { + global $redis; + $redis->lPush('CRON_LOG', json_encode(array( + "time" => time(), + "priority" => $priority, + "task" => $task, + "message" => $message, + ))); +} + +require_once __DIR__ . '/../web/inc/functions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.auth.inc.php'; +require_once __DIR__ . '/../web/inc/sessions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php'; + +// File-lock to prevent concurrent runs +$lock_file = '/tmp/imapsync-oauth-refresh.lock'; +if (file_exists($lock_file)) { + $pid = (int)trim(@file_get_contents($lock_file)); + if ($pid > 0 && posix_kill($pid, 0)) { + logMsg("info", "Previous refresh still running (pid=$pid), exiting"); + exit(0); + } + @unlink($lock_file); +} +file_put_contents($lock_file, getmypid()); + +try { + // client_credentials source tokens, unused per-user tokens (1 day grace), authorization_code per-user tokens; + // failing tokens are retried 15 minutes after their last attempt (C2), errors are sanitized + $imapsync = \Mailcow\Imapsync\ImapsyncModule::create($pdo, \Mailcow\Core\Context::system(), [ + 'hostname' => (string)getenv('MAILCOW_HOSTNAME'), + ]); + foreach ($imapsync->oauth->refreshDue() as $e) { + logMsg($e['priority'], $e['message']); + } +} finally { + @unlink($lock_file); +} diff --git a/data/web/admin/mailbox.php b/data/web/admin/mailbox.php index 7b07d6fa9..a79c68c1a 100644 --- a/data/web/admin/mailbox.php +++ b/data/web/admin/mailbox.php @@ -9,6 +9,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$js_minifier->add('/web/js/site/imapsync.js'); $js_minifier->add('/web/js/site/mailbox.js'); $js_minifier->add('/web/js/presets/sieveMailbox.js'); $js_minifier->add('/web/js/site/pwgen.js'); @@ -42,6 +43,7 @@ $template_data = [ 'lang_rl' => json_encode($lang['ratelimit']), 'lang_edit' => json_encode($lang['edit']), 'lang_datatables' => json_encode($lang['datatables']), + 'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(), ]; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/admin/system.php b/data/web/admin/system.php index b5e00808f..c74f8c6a6 100644 --- a/data/web/admin/system.php +++ b/data/web/admin/system.php @@ -118,6 +118,7 @@ $template_data = [ 'ui_themes' => customize('get', 'ui_themes'), 'ui_theme' => customize('get', 'ui_theme'), 'password_complexity' => password_complexity('get'), + 'imapsync_settings' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSettings(), 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'cors_settings' => $cors_settings, 'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index c86561903..0044ac1e8 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -1465,31 +1465,15 @@ paths: response: value: - log: - - mailbox - - add - syncjob - - active: "1" - automap: "1" - custom_params: "" - delete1: "0" - delete2: "0" - delete2duplicates: "1" - enc1: SSL - exclude: (?i)spam|(?i)junk - host1: imap.server.tld - maxage: "0" - maxbytespersecond: "0" - mins_interval: "20" - password1: supersecret - port1: 993 - skipcrossduplicates: "0" - subfolder2: External - subscribeall: "1" - timeout1: "600" - timeout2: "600" + - add + - job + - username: mailbox@domain.tld + source_id: "3" user1: username - username: mailbox@domain.tld - - null + password1: "*" + mins_interval: "20" + active: "1" msg: - mailbox_modified - mailbox@domain.tld @@ -1497,7 +1481,7 @@ paths: schema: properties: log: - description: contains request object + description: contains request object (secrets are masked as "*") items: {} type: array msg: @@ -1515,8 +1499,10 @@ paths: tags: - Sync jobs description: >- - You can create new sync job using this action. It takes a JSON object - containing a domain informations. + Create a sync job for a local mailbox. Connection data (host, port, + encryption, authentication) comes from the sync source referenced by + `source_id`. Requires the `syncjobs` ACL (user ACL for mailbox users, + domain admin ACL for domain admins). operationId: Create sync job summary: Create sync job requestBody: @@ -1525,11 +1511,9 @@ paths: schema: example: username: lisa@mailcow.tld - host1: mail.mailcow.tld - port1: "143" - user1: demo@mailcow.tld + source_id: "3" + user1: demo@remote.tld password1: supersecretpw - enc1: TLS mins_interval: "20" subfolder2: "/SyncIntoSubfolder" maxage: "0" @@ -1537,44 +1521,47 @@ paths: timeout1: "600" timeout2: "600" exclude: "(?i)spam|(?i)junk" - custom_params: "--dry" + custom_params: + - {o: folder, v: INBOX} + - {o: syncinternaldates, v: ""} delete2duplicates: "1" - delete1: "1" + delete1: "0" delete2: "0" automap: "1" skipcrossduplicates: "0" - subscribeall: "0" + subscribeall: "1" + dry: "0" active: "1" properties: - parameters: - description: your local mailcow mailbox - type: string - host1: - description: the smtp server where mails should be synced from - type: string - port1: - description: the smtp port of the target mail server + username: + description: local mailcow mailbox (destination, user2); users always create jobs for their own mailbox type: string + source_id: + description: >- + id of a sync source visible to the caller (`GET /api/v1/get/syncjob_source/all`). + type: integer user1: - description: the username of the mailbox + description: >- + remote username at the source. For XOAUTH2 authorization_code sources + a token for (source_id, local mailbox, user1) must exist, i.e. the + mailbox owner must have signed in via `/syncjob-oauth` first + (`imapsync_source_oauth_connect_required` otherwise). type: string - password: - description: the password of the mailbox + password1: + description: >- + remote password, required unless the source uses XOAUTH2 (ignored then) type: string - enc1: - description: the encryption method used to connect to the mailserver - type: string - mins_internal: - description: the interval in which messages should be syned + mins_interval: + description: sync interval in minutes (1-43800) type: number subfolder2: description: sync into subfolder on destination (empty = do not use subfolder) type: string maxage: - description: only sync messages up to this age in days + description: only sync messages up to this age in days (0 = ignore age) type: number maxbytespersecond: - description: max speed transfer limit for the sync + description: per-job bandwidth limit in bytes/s (0 = unlimited, capped by the global limit) type: number timeout1: description: timeout for connection to remote host @@ -1586,8 +1573,22 @@ paths: description: exclude objects (regex) type: string custom_params: - description: custom parameters - type: string + description: >- + Allowlisted imapsync options as a list of option/value pairs (must be + an array). + `o` is the option name without leading dashes (see `GET + /api/v1/get/syncjob_options`), `v` the value. The allowlist decides + per option: `true` = a non-empty value is required, `false` = flag + without value (`v` must be empty), string = regular expression the + whole value must match. Values are passed to imapsync as a single argument. + type: array + items: + type: object + properties: + o: + type: string + v: + type: string delete2duplicates: description: delete duplicates on destination (--delete2duplicates) type: boolean @@ -1606,9 +1607,215 @@ paths: subscribeall: description: subscribe all folders (--subscribeall) type: boolean + dry: + description: dry run, nothing is changed (--dry) + type: boolean active: description: enables or disables the sync job type: boolean + host1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + port1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + enc1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + authmech1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + type: object + /api/v1/add/syncjob_source: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - add + - source + - name: Example import + host: imap.example.org + port: "993" + enc: SSL + auth_type: PLAIN + scope: all + msg: + - imapsync_source_added + - Example import + type: success + schema: + properties: + log: + description: contains request object (secrets are masked as "*") + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Create a reusable IMAP sync source. A sync job binds to a source by id; + the source carries host/port/encryption and either static credentials + (PLAIN/LOGIN/CRAM-MD5 — the password is supplied per job) or an OAuth2 + configuration (XOAUTH2). Requires the `syncjobs` ACL. Validation errors + are returned as translated `danger` messages, nothing is written then. + operationId: Create sync job source + summary: Create sync job source + requestBody: + content: + application/json: + schema: + example: + name: Example import + description: "" + host: imap.example.org + port: "993" + enc: SSL + auth_type: PLAIN + active: "1" + scope: domain + domains: + - example.org + properties: + name: + description: >- + source name, max. 100 characters, unique per creator + type: string + description: + description: >- + free text shown in the UI, max. 255 characters + type: string + host: + description: >- + remote IMAP host (hostname or IP, max. 253 characters). + `host1` is accepted as an alias. + type: string + port: + description: >- + remote IMAP port (1-65535). `port1` is accepted as an alias. + type: integer + enc: + description: >- + TLS (STARTTLS) / SSL / PLAIN. `enc1` is accepted as an alias. + type: string + enum: + - TLS + - SSL + - PLAIN + auth_type: + description: >- + authentication mechanism; PLAIN/LOGIN/CRAM-MD5 use the per-job + password, XOAUTH2 uses OAuth2 tokens. XOAUTH2 sources can only be created + and edited by admins and domain admins (`access_denied` for users); users + can use XOAUTH2 sources visible to them in their sync jobs. + type: string + enum: + - PLAIN + - LOGIN + - CRAM-MD5 + - XOAUTH2 + oauth_flow: + description: >- + XOAUTH2 grant type (default client_credentials). client_credentials + sources are always private to their creator (scope is forced to `user` + without targets). + type: string + enum: + - client_credentials + - authorization_code + oauth_authorize_endpoint: + description: >- + authorize URL, required for authorization_code. Must be an https:// URL + without credentials and with a valid hostname + type: string + oauth_token_endpoint: + description: >- + token URL, required for XOAUTH2. Must be an https:// URL without credentials and with a valid hostname + type: string + oauth_userinfo_endpoint: + description: >- + optional identity endpoint for authorization_code (used when the token + response has no usable id_token). Must be an https:// URL without credentials and with a valid hostname + type: string + oauth_client_id: + description: >- + OAuth2 client id, required for XOAUTH2 + type: string + oauth_client_secret: + description: >- + OAuth2 client secret, required for XOAUTH2. Never returned by GET. + type: string + oauth_scope: + description: >- + OAuth2 scope, required for XOAUTH2 + type: string + oauth_extra_params: + description: >- + optional extra token request parameters: a JSON object with scalar values, + sent either as object or as JSON string (empty = none). They never + override the grant fields. + oneOf: + - type: object + additionalProperties: true + - type: string + scope: + description: >- + visibility: `all` (admins only), `domain` (needs `domains`) or `user` + (listed `users`; no targets = private to the creator). Users always + create private sources. + type: string + enum: + - all + - domain + - user + domains: + description: target domains when scope is domain + type: array + items: + type: string + users: + description: target mailboxes when scope is user + type: array + items: + type: string + active: + description: >- + enables or disables the source + type: boolean type: object /api/v1/add/tls-policy-map: post: @@ -2782,6 +2989,62 @@ paths: type: object type: object summary: Delete sync job + /api/v1/delete/syncjob_source: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - delete + - source + - id: + - "3" + msg: + - imapsync_source_deleted + - Example import + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Delete one or more sync job sources by id. Sources still referenced by + a sync job cannot be deleted. Delete or + repoint the dependent jobs first. + operationId: Delete sync job source + summary: Delete sync job source + requestBody: + content: + application/json: + schema: + example: + - "3" + items: + type: string + type: array /api/v1/delete/tls-policy-map: post: responses: @@ -3813,18 +4076,21 @@ paths: examples: response: value: - log: - - entity - - action - - object - msg: - - message - - entity name - type: success + - log: + - syncjob + - edit + - job + - id: + - "1" + active: "0" + msg: + - mailbox_modified + - mailbox@domain.tld + type: success schema: properties: log: - description: contains request object + description: contains request object (secrets are masked as "*") items: {} type: array msg: @@ -3842,8 +4108,8 @@ paths: tags: - Sync jobs description: >- - You can update one or more sync jobs per request. You can also send just - attributes you want to change. + Update one or more sync jobs per request. Only send the attributes you + want to change. Requires the `syncjobs` ACL. operationId: Update sync job requestBody: content: @@ -3852,107 +4118,526 @@ paths: example: attr: active: "1" - automap: "1" - custom_params: "" - delete1: "0" - delete2: "0" - delete2duplicates: "1" - enc1: SSL - exclude: (?i)spam|(?i)junk - host1: imap.server.tld - maxage: "0" - maxbytespersecond: "0" - mins_interval: "20" - password1: supersecret - port1: "993" - skipcrossduplicates: "0" - subfolder2: External - subscribeall: "1" - timeout1: "600" - timeout2: "600" + source_id: "3" user1: username - items: "1" + mins_interval: "20" + custom_params: + - {o: folder, v: INBOX} + items: + - "1" properties: attr: properties: - active: - description: Is sync job active - type: boolean - automap: + source_id: description: >- - Try to automap folders ("Sent items", "Sent" => "Sent" - etc.) - type: boolean - custom_params: - description: Custom parameters passed to imapsync command + id of a sync source visible to the caller (`GET /api/v1/get/syncjob_source/all`). + Visibility is only checked when the source changes. + type: integer + user1: + description: >- + remote username at the source. For XOAUTH2 authorization_code sources + a token for (source_id, local mailbox, user1) must exist, i.e. the + mailbox owner must have signed in via `/syncjob-oauth` first + (`imapsync_source_oauth_connect_required` otherwise). For XOAUTH2 + client_credentials sources user1 may only be changed by callers who can + see the source (`access_denied` otherwise); the other fields stay editable. type: string + password1: + description: >- + remote password; empty keeps the stored one. Required when the job + is moved to a non-XOAUTH2 source and no password is stored. + type: string + mins_interval: + description: sync interval in minutes (1-43800) + type: number + subfolder2: + description: sync into subfolder on destination (empty = do not use subfolder) + type: string + maxage: + description: only sync messages up to this age in days (0 = ignore age) + type: number + maxbytespersecond: + description: per-job bandwidth limit in bytes/s (0 = unlimited, capped by the global limit) + type: number + timeout1: + description: timeout for connection to remote host + type: number + timeout2: + description: timeout for connection to local host + type: number + exclude: + description: exclude objects (regex) + type: string + custom_params: + description: >- + Allowlisted imapsync options as a list of option/value pairs (must be + an array). + `o` is the option name without leading dashes (see `GET + /api/v1/get/syncjob_options`), `v` the value. The allowlist decides + per option: `true` = a non-empty value is required, `false` = flag + without value (`v` must be empty), string = regular expression the + whole value must match. Options taking file paths are never allowed. + Values are passed to imapsync as a single argument. + type: array + items: + type: object + properties: + o: + type: string + v: + type: string + prio: + description: >- + priority (admin only, ignored otherwise). Due jobs run ordered by + prio DESC, never-run first, last_run ASC, id ASC. + type: number + last_run: + description: any value resets last_run (job becomes due) + type: string + success: + description: any value resets the last result + type: string + delete2duplicates: + description: delete duplicates on destination (--delete2duplicates) + type: boolean delete1: - description: Delete from source when completed + description: delete from source when completed (--delete1) type: boolean delete2: - description: Delete messages on destination that are not on source + description: delete messages on destination that are not on source (--delete2) type: boolean - delete2duplicates: - description: Delete duplicates on destination + automap: + description: try to automap folders ("Sent items", "Sent" => "Sent" etc.) (--automap) type: boolean + skipcrossduplicates: + description: skip duplicate messages across folders (first come, first serve) (--skipcrossduplicates) + type: boolean + subscribeall: + description: subscribe all folders (--subscribeall) + type: boolean + dry: + description: dry run, nothing is changed (--dry) + type: boolean + active: + description: enables or disables the sync job + type: boolean + host1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + port1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string enc1: - description: Encryption + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + authmech1: + description: >- + Removed. Host, port, encryption and authentication now come from + the sync source (`source_id`, see `/api/v1/add/syncjob_source`). + The attribute is ignored. + deprecated: true + type: string + order_id: + description: Removed, use `prio` (admin) or `/api/v1/edit/syncjob/run_next`. Ignored. + deprecated: true + type: number + type: object + items: + description: list of sync job ids to update + type: array + items: + type: string + type: object + summary: Update sync job + /api/v1/edit/syncjob/run_next: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - edit + - run_next + - id: + - "1" + msg: + - imapsync_run_next_done + - 1 + type: success + schema: + properties: + log: + description: contains request object (secrets are masked as "*") + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Admin only. Moves the given jobs to the front of the queue: `prio` is set + above the current maximum and `last_run` is cleared, so the runner picks + them next. Jobs that are currently running are rejected with + `imapsync_job_running` (one message per job), the others are moved. + The jobs are locked while they are moved, so a job the runner claims at + the same time is reported as running instead of being counted. + operationId: Run sync job next + summary: Run sync job next + requestBody: + content: + application/json: + schema: + example: + items: + - "1" + attr: {} + properties: + items: + description: list of sync job ids + type: array + items: + type: string + attr: + description: unused, send an empty object + type: object + type: object + /api/v1/edit/syncjob_source: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - edit + - source + - id: + - "3" + name: Example import + oauth_client_secret: "*" + msg: + - imapsync_source_modified + - Example import + type: success + schema: + properties: + log: + description: contains request object (secrets are masked as "*") + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Update one or more sync sources (creator, admins, and domain admins for + sources created by mailboxes of their domains). Fields not provided are + left unchanged. All checks run before anything is written; a rejected + edit changes nothing. Requires the `syncjobs` ACL. + operationId: Update sync job source + summary: Update sync job source + requestBody: + content: + application/json: + schema: + example: + items: + - "3" + attr: + name: Example import + host: imap.example.org + port: "993" + enc: SSL + auth_type: PLAIN + active: "1" + properties: + attr: + properties: + name: + description: >- + source name, max. 100 characters, unique per creator + type: string + description: + description: >- + free text shown in the UI, max. 255 characters + type: string + host: + description: >- + remote IMAP host (hostname or IP, max. 253 characters). + `host1` is accepted as an alias. Host, port and enc are only validated + when they change (migrated legacy values stay editable). + type: string + port: + description: >- + remote IMAP port (1-65535). `port1` is accepted as an alias. + type: integer + enc: + description: >- + TLS (STARTTLS) / SSL / PLAIN. `enc1` is accepted as an alias. + type: string enum: - TLS - SSL - PLAIN - type: string - exclude: - description: Exclude objects (regex) - type: string - host1: - description: Hostname - type: string - maxage: + auth_type: description: >- - Maximum age of messages in days that will be polled from - remote (0 = ignore age) - type: number - maxbytespersecond: - description: Max. bytes per second (0 = unlimited) - type: number - mins_interval: - description: Interval (min) - type: number - password1: - description: Password + authentication mechanism; PLAIN/LOGIN/CRAM-MD5 use the per-job + password, XOAUTH2 uses OAuth2 tokens. XOAUTH2 sources can only be created + and edited by admins and domain admins (`access_denied` for users); users + can use XOAUTH2 sources visible to them in their sync jobs. type: string - port1: - description: Port - type: string - skipcrossduplicates: + enum: + - PLAIN + - LOGIN + - CRAM-MD5 + - XOAUTH2 + oauth_flow: description: >- - Skip duplicate messages across folders (first come, - first serve) + XOAUTH2 grant type (default client_credentials). client_credentials + sources are always private to their creator (scope is forced to `user` + without targets). + type: string + enum: + - client_credentials + - authorization_code + oauth_authorize_endpoint: + description: >- + authorize URL, required for authorization_code. Must be an https:// URL + without credentials and with a valid hostname + type: string + oauth_token_endpoint: + description: >- + token URL, required for XOAUTH2. Must be an https:// URL without credentials and with a valid hostname + type: string + oauth_userinfo_endpoint: + description: >- + optional identity endpoint for authorization_code (used when the token + response has no usable id_token). Must be an https:// URL without credentials and with a valid hostname + type: string + oauth_client_id: + description: >- + OAuth2 client id, required for XOAUTH2 + type: string + oauth_client_secret: + description: >- + OAuth2 client secret; empty keeps the stored one. Changing auth_type, + oauth_flow, oauth_token_endpoint, oauth_client_id, host, port or enc drops + the cached source token and all per-user tokens of the source (users have + to sign in again). + type: string + oauth_scope: + description: >- + OAuth2 scope, required for XOAUTH2 + type: string + oauth_extra_params: + description: >- + optional extra token request parameters: a JSON object with scalar values, + sent either as object or as JSON string (empty = none). They never + override the grant fields. + oneOf: + - type: object + additionalProperties: true + - type: string + scope: + description: >- + visibility: `all` (admins only), `domain` (needs `domains`) or `user` + (listed `users`; no targets = private to the creator). Users always + create private sources. Targets are only rewritten when `scope` is sent. + type: string + enum: + - all + - domain + - user + domains: + description: target domains when scope is domain + type: array + items: + type: string + users: + description: target mailboxes when scope is user + type: array + items: + type: string + active: + description: >- + enables or disables the source type: boolean - subfolder2: - description: >- - Sync into subfolder on destination (empty = do not use - subfolder) - type: string - subscribeall: - description: Subscribe all folders - type: boolean - timeout1: - description: Timeout for connection to remote host - type: number - timeout2: - description: Timeout for connection to local host - type: number - user1: - description: Username - type: string type: object items: - description: contains list of aliases you want update + description: list of source ids to update + type: array + items: + type: string + type: object + /api/v1/edit/syncjob_source/refresh_token: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - edit + - refresh_token + - id: + - "3" + msg: + - imapsync_source_token_refreshed + - Example import + type: success + schema: + properties: + log: + description: contains request object (secrets are masked as "*") + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Force an OAuth2 access-token refresh for a XOAUTH2 client_credentials + source (other sources: `imapsync_source_refresh_unsupported`). Requires + edit rights on the source and the `syncjobs` ACL. + operationId: Refresh sync job source token + summary: Refresh sync job source token + requestBody: + content: + application/json: + schema: + example: + items: + - "3" + properties: + items: + description: source id (only the first one is used) + type: array + items: + type: string + type: object + /api/v1/edit/imapsync_settings: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - syncjob + - edit + - settings + - max_parallel: "4" + max_kb_per_second: "2000" + msg: max_parallel_saved + type: success + schema: + properties: + log: + description: contains request object (secrets are masked as "*") + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Update global sync job settings (admin only). `max_parallel` is the + number of imapsync processes that may run concurrently (1-32); + `max_kb_per_second` is a per-process bandwidth cap in KB/s (0 = + unlimited, max. 10485760 = 10 GiB/s), stored internally in bytes/s. + operationId: Update sync job settings + summary: Update sync job settings + requestBody: + content: + application/json: + schema: + example: + attr: + max_parallel: "4" + max_kb_per_second: "2000" + properties: + attr: + properties: + max_parallel: + description: max. concurrent sync processes (1-32) + type: number + max_kb_per_second: + description: bandwidth limit per process in KB/s (0 = unlimited, max. 10485760) + type: number type: object type: object - summary: Update sync job /api/v1/edit/user-acl: post: responses: @@ -5607,6 +6292,14 @@ paths: summary: Get version status /api/v1/get/syncjobs/all/no_log: get: + parameters: + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string responses: "401": $ref: "#/components/responses/Unauthorized" @@ -5616,43 +6309,196 @@ paths: examples: response: value: - - active: "1" - authmd51: 0 - authmech1: PLAIN + - id: 1 + user2: mailbox@domain.tld + source_id: 3 + user1: username + active: 1 automap: 1 - created: "2019-05-22 11:37:25" - custom_params: "" + custom_params: '[{"o":"folder","v":"INBOX"}]' + custom_params_legacy: null delete1: 0 delete2: 0 delete2duplicates: 1 - domain2: "" - enc1: TLS + dry: 0 exclude: (?i)spam|(?i)junk - host1: imap.server.tld - id: 1 + exit_status: EX_OK is_running: 0 - last_run: "2019-05-22 11:40:02" + last_run: "2026-10-15 11:40:02" + success: 1 log: "" maxage: 0 maxbytespersecond: "0" - mins_interval: "20" - modified: "2019-05-22 11:40:02" - port1: 993 - regextrans2: "" + mins_interval: 20 + prio: 0 skipcrossduplicates: 0 subfolder2: External subscribeall: 1 timeout1: 600 timeout2: 600 - user1: username - user2: mailbox@domain.tld + created: "2026-10-01 11:37:25" + modified: "2026-10-15 11:40:02" + source_name: Example import + source_description: "" + source_host: imap.example.org + source_port: 993 + source_enc: SSL + source_auth_type: PLAIN + source_oauth_flow: client_credentials + source_created_by: "" + source_scope: all + source_active: 1 + source_oauth_token_expires: null + source_oauth_last_refresh_error: null + user_token_exists: 0 + user_token_expires: null + user_token_error: null description: OK headers: {} tags: - Sync jobs - description: You can list all syn jobs existing in system. + description: >- + Lists all sync jobs the caller may access (`/api/v1/get/syncjobs/all` + includes the log, `/api/v1/get/syncjobs/[/no_log]` filters by + mailbox). Jobs are ordered by prio DESC, never-run first, last_run ASC, + id ASC. + `custom_params_legacy` holds options of migrated jobs that could not be + converted (informational, not used). The removed fields `host1`, + `port1`, `enc1` and `authmech1` are no longer part of a job. operationId: Get sync jobs summary: Get sync jobs + "/api/v1/get/syncjob_source/{id}": + get: + parameters: + - description: source id, or `all` to list every source visible to the session (sorted by name) + example: all + in: path + name: id + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - id: 3 + name: Example import + description: "" + created_by: "" + scope: domain + domains: + - example.org + users: [] + host: imap.example.org + port: 993 + enc: SSL + host1: imap.example.org + port1: 993 + enc1: SSL + auth_type: XOAUTH2 + oauth_flow: authorization_code + oauth_token_endpoint: https://login.example.org/token + oauth_authorize_endpoint: https://login.example.org/authorize + oauth_userinfo_endpoint: null + oauth_client_id: 0000-1111 + oauth_scope: https://outlook.office.com/IMAP.AccessAsUser.All offline_access + oauth_extra_params: null + oauth_redirect_uri: https://mail.example.org/syncjob-oauth + oauth_token_expires: null + oauth_last_refresh_error: null + oauth_last_refresh_attempt: null + has_client_secret: true + has_access_token: false + can_edit: true + active: 1 + created: "2026-10-01 09:00:00" + modified: "2026-10-01 09:00:00" + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Returns a single sync source by id, or all sources visible to the caller + when `id` is `all`. `can_edit` tells whether the caller may edit/delete + the source. + operationId: Get sync job sources + summary: Get sync job sources + /api/v1/get/syncjob_options: + get: + parameters: + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + dry: false + folder: true + maxsize: true + sslargs1: ^SSL_verify_mode=[01]$ + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Returns the allowlist of imapsync options accepted in a sync job's + `custom_params` as option => rule: `true` = requires a non-empty value, + `false` = flag without value, string = regular expression the whole + value must match. + operationId: Get sync job options + summary: Get sync job options + /api/v1/get/imapsync_settings: + get: + parameters: + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + max_parallel: 4 + max_bytes_per_second: 2048000 + max_kb_per_second: 2000 + description: OK + headers: {} + tags: + - Sync jobs + description: >- + Admin only. Returns the global sync job settings (`{}` for other roles). + operationId: Get sync job settings + summary: Get sync job settings "/api/v1/get/tls-policy-map/{id}": get: parameters: diff --git a/data/web/domainadmin/mailbox.php b/data/web/domainadmin/mailbox.php index 8beb5abb9..cc1f5bc1b 100644 --- a/data/web/domainadmin/mailbox.php +++ b/data/web/domainadmin/mailbox.php @@ -9,6 +9,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$js_minifier->add('/web/js/site/imapsync.js'); $js_minifier->add('/web/js/site/mailbox.js'); $js_minifier->add('/web/js/presets/sieveMailbox.js'); $js_minifier->add('/web/js/site/pwgen.js'); @@ -42,6 +43,7 @@ $template_data = [ 'lang_rl' => json_encode($lang['ratelimit']), 'lang_edit' => json_encode($lang['edit']), 'lang_datatables' => json_encode($lang['datatables']), + 'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(), ]; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; \ No newline at end of file diff --git a/data/web/edit.php b/data/web/edit.php index 48f2309c1..b003d5bf6 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -204,9 +204,15 @@ if (isset($_SESSION['mailcow_cc_role'])) { if (isset($_GET['syncjob']) && is_numeric($_GET['syncjob'])) { $id = $_GET["syncjob"]; - $result = mailbox('get', 'syncjob_details', $id); + $result = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJob($id); $template = 'edit/syncjob.twig'; } + elseif (isset($_GET['syncjob_source']) && + is_numeric($_GET['syncjob_source'])) { + $id = $_GET["syncjob_source"]; + $result = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSource($id); + $template = 'edit/imapsync_source.twig'; + } elseif (isset($_GET['filter']) && is_numeric($_GET['filter'])) { $id = $_GET["filter"]; @@ -225,6 +231,7 @@ else { $template_data['access_denied'] = true; } +$js_minifier->add('/web/js/site/imapsync.js'); $js_minifier->add('/web/js/site/edit.js'); $js_minifier->add('/web/js/site/pwgen.js'); @@ -233,5 +240,6 @@ $template_data['return_to'] = $_SESSION['return_to']; $template_data['lang_user'] = json_encode($lang['user']); $template_data['lang_admin'] = json_encode($lang['admin']); $template_data['lang_datatables'] = json_encode($lang['datatables']); +$template_data['imapsync_redirect_uri'] = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/inc/ajax/syncjob_logs.php b/data/web/inc/ajax/syncjob_logs.php index 9c521aec7..5caf2e9f8 100644 --- a/data/web/inc/ajax/syncjob_logs.php +++ b/data/web/inc/ajax/syncjob_logs.php @@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { } if (isset($_GET['id']) && is_numeric($_GET['id'])) { - if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) { + if ($details = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJob(intval($_GET['id']))) { echo (empty($details['log'])) ? '-' : $details['log']; } } diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 4ced152ae..01dc2c532 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -33,11 +33,11 @@ if (array_key_exists('pending_tfa_methods', $_SESSION)) { if (isset($pending_tfa_authmechs['webauthn'])) { $pending_tfa_authmechs['webauthn'] = true; } - if (!isset($pending_tfa_authmechs['webauthn']) + if (!isset($pending_tfa_authmechs['webauthn']) && isset($pending_tfa_authmechs['yubi_otp'])) { $pending_tfa_authmechs['yubi_otp'] = true; } - if (!isset($pending_tfa_authmechs['webauthn']) + if (!isset($pending_tfa_authmechs['webauthn']) && !isset($pending_tfa_authmechs['yubi_otp']) && isset($pending_tfa_authmechs['totp'])) { $pending_tfa_authmechs['totp'] = true; @@ -72,6 +72,7 @@ $globalVariables = [ 'lang_fido2' => json_encode($lang['fido2']), 'lang_success' => json_encode($lang['success']), 'lang_danger' => json_encode($lang['danger']), + 'lang_syncjobs' => json_encode($lang['syncjobs']), 'docker_timeout' => $DOCKER_TIMEOUT, 'session_lifetime' => (int)$SESSION_LIFETIME, 'csrf_token' => $_SESSION['CSRF']['TOKEN'], diff --git a/data/web/inc/functions.admin.inc.php b/data/web/inc/functions.admin.inc.php index 7bd0af42f..69f1b7e56 100644 --- a/data/web/inc/functions.admin.inc.php +++ b/data/web/inc/functions.admin.inc.php @@ -103,7 +103,7 @@ function admin($_action, $_data = null) { continue; } } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data_log), @@ -179,6 +179,9 @@ function admin($_action, $_data = null) { $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); } + if ($username_new != $username) { + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new); + } } $_SESSION['return'][] = array( 'type' => 'success', @@ -211,6 +214,7 @@ function admin($_action, $_data = null) { $stmt->execute(array( ':username' => $username, )); + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username); $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); $stmt->execute(array( ':username' => $username, diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 17aa4ad6a..e2261ea70 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -156,7 +156,7 @@ function domain_admin($_action, $_data = null) { } } } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data_log), @@ -183,6 +183,9 @@ function domain_admin($_action, $_data = null) { ':username_new' => $username_new, ':username' => $username )); + if ($username_new != $username) { + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new); + } if (!empty($domains)) { foreach ($domains as $domain) { $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) @@ -331,6 +334,8 @@ function domain_admin($_action, $_data = null) { $stmt->execute(array( ':username' => $username, )); + // sync sources created by this account must not pass to a re-created account of the same name + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_data_log), diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 37ccbd85c..645e96a4d 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -302,192 +302,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => array('mailbox_modified', $username) ); break; - case 'syncjob': - if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - else { - $username = $_data['username']; - } - } - elseif ($_SESSION['mailcow_cc_role'] == "user") { - $username = $_SESSION['mailcow_cc_username']; - } - else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'no_user_defined' - ); - return false; - } - $active = intval($_data['active']); - $subscribeall = intval($_data['subscribeall']); - $delete2duplicates = intval($_data['delete2duplicates']); - $delete1 = intval($_data['delete1']); - $delete2 = intval($_data['delete2']); - $timeout1 = intval($_data['timeout1']); - $timeout2 = intval($_data['timeout2']); - $skipcrossduplicates = intval($_data['skipcrossduplicates']); - $automap = intval($_data['automap']); - $dry = intval($_data['dry']); - $port1 = $_data['port1']; - $host1 = strtolower($_data['host1']); - $password1 = $_data['password1']; - $exclude = $_data['exclude']; - $maxage = $_data['maxage']; - $maxbytespersecond = $_data['maxbytespersecond']; - $subfolder2 = $_data['subfolder2']; - $user1 = $_data['user1']; - $mins_interval = $_data['mins_interval']; - $enc1 = $_data['enc1']; - $custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); - - // validate custom params - foreach (explode('-', $custom_params) as $param){ - if(empty($param)) continue; - - // extract option - if (str_contains($param, '=')) $param = explode('=', $param)[0]; - else $param = rtrim($param, ' '); - // remove first char if first char is - - if ($param[0] == '-') $param = ltrim($param, $param[0]); - - if (str_contains($param, ' ')) { - // bad char - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'bad character SPACE' - ); - return false; - } - - // check if param is whitelisted - if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ - // bad option - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'bad option '. $param - ); - return false; - } - } - if (empty($subfolder2)) { - $subfolder2 = ""; - } - if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $maxage = "0"; - } - if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $timeout1 = "600"; - } - if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $timeout2 = "600"; - } - if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) { - $maxbytespersecond = "0"; - } - if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - // if (!is_valid_domain_name($host1)) { - // $_SESSION['return'][] = array( - // 'type' => 'danger', - // 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - // 'msg' => 'access_denied' - // ); - // return false; - // } - if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - if (@preg_match("/" . $exclude . "/", null) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->prepare("SELECT '1' FROM `imapsync` - WHERE `user2` = :user2 AND `user1` = :user1 AND `host1` = :host1"); - $stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1)); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('object_exists', htmlspecialchars($host1 . ' / ' . $user1)) - ); - return false; - } - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`) - VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)"); - $stmt->execute(array( - ':user2' => $username, - ':custom_params' => $custom_params, - ':exclude' => $exclude, - ':maxage' => $maxage, - ':delete1' => $delete1, - ':delete2' => $delete2, - ':timeout1' => $timeout1, - ':timeout2' => $timeout2, - ':automap' => $automap, - ':skipcrossduplicates' => $skipcrossduplicates, - ':maxbytespersecond' => $maxbytespersecond, - ':subscribeall' => $subscribeall, - ':dry' => $dry, - ':subfolder2' => $subfolder2, - ':host1' => $host1, - ':authmech1' => 'PLAIN', - ':user1' => $user1, - ':password1' => $password1, - ':mins_interval' => $mins_interval, - ':port1' => $port1, - ':enc1' => $enc1, - ':delete2duplicates' => $delete2duplicates, - ':active' => $active, - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('mailbox_modified', $username) - ); - break; case 'domain': if ($_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'][] = array( @@ -2280,202 +2094,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'syncjob': - if (!is_array($_data['id'])) { - $ids = array(); - $ids[] = $_data['id']; - } - else { - $ids = $_data['id']; - } - if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - foreach ($ids as $id) { - $is_now = mailbox('get', 'syncjob_details', $id, array('with_password')); - if (!empty($is_now)) { - $username = $is_now['user2']; - $user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1']; - $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; - $last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run']; - $success = (isset($_data['success'])) ? NULL : $is_now['success']; - $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates']; - $subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall']; - $dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry']; - $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1']; - $delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2']; - $automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap']; - $skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates']; - $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1']; - $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; - $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1']; - $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2']; - $enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1']; - $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval']; - $exclude = (isset($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude']; - $custom_params = (isset($_data['custom_params'])) ? $_data['custom_params'] : $is_now['custom_params']; - $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage']; - $maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond']; - $timeout1 = (isset($_data['timeout1']) && $_data['timeout1'] != "") ? intval($_data['timeout1']) : $is_now['timeout1']; - $timeout2 = (isset($_data['timeout2']) && $_data['timeout2'] != "") ? intval($_data['timeout2']) : $is_now['timeout2']; - } - else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - - // validate custom params - foreach (explode('-', $custom_params) as $param){ - if(empty($param)) continue; - - // extract option - if (str_contains($param, '=')) $param = explode('=', $param)[0]; - else $param = rtrim($param, ' '); - // remove first char if first char is - - if ($param[0] == '-') $param = ltrim($param, $param[0]); - - if (str_contains($param, ' ')) { - // bad char - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'bad character SPACE' - ); - return false; - } - - // check if param is whitelisted - if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ - // bad option - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'bad option '. $param - ); - return false; - } - } - if (empty($subfolder2)) { - $subfolder2 = ""; - } - if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $maxage = "0"; - } - if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $timeout1 = "600"; - } - if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { - $timeout2 = "600"; - } - if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) { - $maxbytespersecond = "0"; - } - if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - if (!is_valid_domain_name($host1)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - if (@preg_match("/" . $exclude . "/", null) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, - `delete2` = :delete2, - `automap` = :automap, - `skipcrossduplicates` = :skipcrossduplicates, - `maxage` = :maxage, - `maxbytespersecond` = :maxbytespersecond, - `subfolder2` = :subfolder2, - `exclude` = :exclude, - `host1` = :host1, - `last_run` = :last_run, - `success` = :success, - `user1` = :user1, - `password1` = :password1, - `mins_interval` = :mins_interval, - `port1` = :port1, - `enc1` = :enc1, - `delete2duplicates` = :delete2duplicates, - `custom_params` = :custom_params, - `timeout1` = :timeout1, - `timeout2` = :timeout2, - `subscribeall` = :subscribeall, - `dry` = :dry, - `active` = :active - WHERE `id` = :id"); - $stmt->execute(array( - ':delete1' => $delete1, - ':delete2' => $delete2, - ':automap' => $automap, - ':skipcrossduplicates' => $skipcrossduplicates, - ':id' => $id, - ':exclude' => $exclude, - ':maxage' => $maxage, - ':maxbytespersecond' => $maxbytespersecond, - ':subfolder2' => $subfolder2, - ':host1' => $host1, - ':user1' => $user1, - ':password1' => $password1, - ':last_run' => $last_run, - ':success' => $success, - ':mins_interval' => $mins_interval, - ':port1' => $port1, - ':enc1' => $enc1, - ':delete2duplicates' => $delete2duplicates, - ':custom_params' => $custom_params, - ':timeout1' => $timeout1, - ':timeout2' => $timeout2, - ':subscribeall' => $subscribeall, - ':dry' => $dry, - ':active' => $active, - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('mailbox_modified', $username) - ); - } - break; case 'filter': if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) { $_SESSION['return'][] = array( @@ -3652,6 +3270,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } } + // Sync sources created by / targeting the mailbox and its OAuth tokens (owner); a PDOException rolls back below + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->sources->onAccountRenamed($old_username, $new_username); + // Update c_uid, c_name and mail in _sogo_static_view table $pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username") ->execute([ @@ -4603,68 +4224,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return false; break; - case 'syncjob_details': - $syncjobdetails = array(); - if (!is_numeric($_data)) { - return false; - } - if (isset($_extra) && in_array('no_log', $_extra)) { - $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")'); - $fields = $field_query->fetchAll(PDO::FETCH_ASSOC); - while($field = array_shift($fields)) { - $shown_fields[] = $field['Field']; - } - $stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ", - `active` - FROM `imapsync` WHERE id = :id"); - } - elseif (isset($_extra) && in_array('with_password', $_extra)) { - $stmt = $pdo->prepare("SELECT *, - `active` - FROM `imapsync` WHERE id = :id"); - } - else { - $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")'); - $fields = $field_query->fetchAll(PDO::FETCH_ASSOC); - while($field = array_shift($fields)) { - $shown_fields[] = $field['Field']; - } - $stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ", - `active` - FROM `imapsync` WHERE id = :id"); - } - $stmt->execute(array(':id' => $_data)); - $syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($syncjobdetails['returned_text'])) { - $syncjobdetails['log'] = $syncjobdetails['returned_text']; - } - else { - $syncjobdetails['log'] = ''; - } - unset($syncjobdetails['returned_text']); - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $syncjobdetails['user2'])) { - return false; - } - return $syncjobdetails; - break; - case 'syncjobs': - $syncjobdata = array(); - if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { - return false; - } - } - else { - $_data = $_SESSION['mailcow_cc_username']; - } - $stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username"); - $stmt->execute(array(':username' => $_data)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $syncjobdata[] = $row['id']; - } - return $syncjobdata; - break; case 'spam_score': $curl = curl_init(); curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock'); @@ -5494,46 +5053,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { break; case 'delete': switch ($_type) { - case 'syncjob': - if (!is_array($_data['id'])) { - $ids = array(); - $ids[] = $_data['id']; - } - else { - $ids = $_data['id']; - } - if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } - foreach ($ids as $id) { - if (!is_numeric($id)) { - return false; - } - $stmt = $pdo->prepare("SELECT `user2` FROM `imapsync` WHERE id = :id"); - $stmt->execute(array(':id' => $id)); - $user2 = $stmt->fetch(PDO::FETCH_ASSOC)['user2']; - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $user2)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - continue; - } - $stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `id`= :id"); - $stmt->execute(array(':id' => $id)); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('deleted_syncjob', $id) - ); - } - break; case 'filter': if (!is_array($_data['id'])) { $ids = array(); @@ -5764,6 +5283,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':domain' => $domain, )); + $stmt = $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain, + )); $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain"); $stmt->execute(array( ':domain' => $domain, @@ -6071,6 +5594,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); + // Tokens, scope targets and own sync sources of the mailbox + \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username); $stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username"); $stmt->execute(array( ':username' => $username diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index f6b0cd792..a9344e6c0 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -4,7 +4,7 @@ function init_db_schema() try { global $pdo; - $db_version = "18082026_1200"; + $db_version = "16102026_1200"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -755,35 +755,153 @@ function init_db_schema() ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "imapsync_source" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "name" => "VARCHAR(100) NOT NULL", + "description" => "VARCHAR(255) NOT NULL DEFAULT ''", + "created_by" => "VARCHAR(255) NOT NULL DEFAULT ''", + "scope" => "ENUM('all','domain','user') NOT NULL DEFAULT 'user'", + "host1" => "VARCHAR(255) NOT NULL", + "port1" => "SMALLINT UNSIGNED NOT NULL", + "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", + "auth_type" => "ENUM('PLAIN','LOGIN','CRAM-MD5','XOAUTH2') DEFAULT 'PLAIN'", + "oauth_flow" => "ENUM('client_credentials','authorization_code') NOT NULL DEFAULT 'client_credentials'", + "oauth_token_endpoint" => "VARCHAR(500) DEFAULT NULL", + "oauth_authorize_endpoint" => "VARCHAR(500) DEFAULT NULL", + "oauth_userinfo_endpoint" => "VARCHAR(500) DEFAULT NULL", + "oauth_client_id" => "VARCHAR(500) DEFAULT NULL", + "oauth_client_secret" => "VARCHAR(500) DEFAULT NULL", + "oauth_scope" => "VARCHAR(500) DEFAULT NULL", + "oauth_extra_params" => "TEXT DEFAULT NULL", + "oauth_access_token" => "TEXT DEFAULT NULL", + "oauth_token_expires" => "BIGINT UNSIGNED DEFAULT NULL", + "oauth_last_refresh_error" => "TEXT DEFAULT NULL", + "oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "uniq_creator_name" => array("created_by", "name") + ), + "key" => array( + "idx_created_by" => array("created_by"), + "idx_scope" => array("scope") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync_source_domain" => array( + "cols" => array( + "source_id" => "INT NOT NULL", + "domain" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("source_id", "domain") + ), + "key" => array( + "idx_domain" => array("domain") + ), + "fkey" => array( + "fk_imapsync_source_domain" => array( + "col" => "source_id", + "ref" => "imapsync_source.id", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync_source_user" => array( + "cols" => array( + "source_id" => "INT NOT NULL", + "username" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("source_id", "username") + ), + "key" => array( + "idx_username" => array("username") + ), + "fkey" => array( + "fk_imapsync_source_user" => array( + "col" => "source_id", + "ref" => "imapsync_source.id", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync_source_oauth_token" => array( + "cols" => array( + "source_id" => "INT NOT NULL", + "owner" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "access_token" => "TEXT DEFAULT NULL", + "refresh_token" => "TEXT DEFAULT NULL", + "token_expires" => "BIGINT UNSIGNED DEFAULT NULL", + "last_refresh_error" => "TEXT DEFAULT NULL", + "oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("source_id", "owner", "username") + ), + "key" => array( + "idx_owner" => array("owner") + ), + "fkey" => array( + "fk_imapsync_source_oauth_token" => array( + "col" => "source_id", + "ref" => "imapsync_source.id", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "imapsync" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", "user2" => "VARCHAR(255) NOT NULL", - "host1" => "VARCHAR(255) NOT NULL", - "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", + "source_id" => "INT NOT NULL", "regextrans2" => "VARCHAR(255) DEFAULT ''", "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", "user1" => "VARCHAR(255) NOT NULL", - "password1" => "VARCHAR(255) NOT NULL", + "password1" => "VARCHAR(255) NOT NULL DEFAULT ''", "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", "maxage" => "SMALLINT NOT NULL DEFAULT '0'", "mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'", "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'", - "port1" => "SMALLINT UNSIGNED NOT NULL", - "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'", "automap" => "TINYINT(1) NOT NULL DEFAULT '0'", "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'", - "custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''", + "custom_params" => "TEXT", + // original legacy custom_params string, only set when options were dropped during migration (never used by the runner) + "custom_params_legacy" => "TEXT DEFAULT NULL", "timeout1" => "SMALLINT NOT NULL DEFAULT '600'", "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", "dry" => "TINYINT(1) NOT NULL DEFAULT '0'", "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", + "prio" => "INT NOT NULL DEFAULT 0", "returned_text" => "LONGTEXT", "last_run" => "TIMESTAMP NULL DEFAULT NULL", "success" => "TINYINT(1) UNSIGNED DEFAULT NULL", @@ -795,6 +913,32 @@ function init_db_schema() "keys" => array( "primary" => array( "" => array("id") + ), + "key" => array( + "idx_source_id" => array("source_id"), + "idx_prio" => array("prio") + ), + "fkey" => array( + "fk_imapsync_source" => array( + "col" => "source_id", + "ref" => "imapsync_source.id", + "delete" => "RESTRICT", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync_settings" => array( + "cols" => array( + "name" => "VARCHAR(64) NOT NULL", + "value" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("name") ) ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" @@ -1184,11 +1328,128 @@ function init_db_schema() } } + // Migrate legacy imapsync jobs (host/port/enc per job) to domain-scoped legacy sources + // Re-entrant: only rows with source_id IS NULL are migrated, the legacy columns are kept while any are left + if ($table == 'imapsync') { + $stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'"); + if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) { + $imapsync_cols = array_column($pdo->query("SHOW COLUMNS FROM `imapsync`")->fetchAll(PDO::FETCH_ASSOC), 'Field', 'Field'); + if (isset($imapsync_cols['host1'])) { + if (!isset($imapsync_cols['source_id'])) { + $pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL"); + } + if (!isset($imapsync_cols['custom_params_legacy'])) { + $pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `custom_params_legacy` TEXT DEFAULT NULL"); + } + // Converted JSON may not fit into the old VARCHAR(512) + $pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `custom_params` TEXT"); + $imapsync_allow = \Mailcow\Imapsync\Model\CustomParams::allowlist($GLOBALS['IMAPSYNC_OPTIONS'] ?? [], $GLOBALS['IMAPSYNC_OPTIONS_DEFAULT'] ?? []); + $source_sel = $pdo->prepare("SELECT `id` FROM `imapsync_source` WHERE `created_by` = '' AND `name` = :name"); + $source_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source` (`name`, `description`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `active`) + VALUES (:name, '', '', 'domain', :host, :port, :enc, :auth, 1)"); + $source_domain_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)"); + $job_upd = $pdo->prepare("UPDATE `imapsync` SET `source_id` = :source_id, `custom_params` = :cp, `custom_params_legacy` = :cp_legacy, + `dry` = GREATEST(`dry`, :dry) WHERE `id` = :id AND `source_id` IS NULL"); + $stmt = $pdo->query("SELECT `id`, `user2`, `host1`, `port1`, `enc1`, `custom_params` FROM `imapsync` WHERE `source_id` IS NULL"); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + // Parse the legacy custom_params string like the old runner did (all tokens up to the next option are its value) + // into pairs valid under $IMAPSYNC_OPTIONS, a token after a flag is never its value. + // --authmech1 PLAIN|LOGIN|CRAM-MD5 becomes the source auth type, --dry the job's dry flag (never run a simulation for real). + // Anything else that does not fit is dropped, the original string is then kept in custom_params_legacy. + $pairs = array(); + $auth = null; + $dry = false; + $dropped = false; + $tokens = preg_split('/\s+/', trim((string)$row['custom_params']), -1, PREG_SPLIT_NO_EMPTY); + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + if ($tokens[$i][0] !== '-') { + // stray value without option + $dropped = true; + continue; + } + @list($opt, $val) = explode('=', ltrim($tokens[$i], '-'), 2); + $opt = strtolower($opt); + $inline = ($val !== null); + $rest = array(); + while ($i + 1 < $n && $tokens[$i + 1][0] !== '-') { + $rest[] = $tokens[++$i]; + } + $rule = ($opt === 'authmech1') ? true : ($imapsync_allow[$opt] ?? null); + if ($opt === 'dry' || $rule === false) { + if ($inline || !empty($rest)) $dropped = true; + if ($opt === 'dry') { + $dry = true; + } elseif (!$inline) { + $pairs[] = array('o' => $opt, 'v' => ''); + } + continue; + } + // unknown option, or "--opt=a b" (the old runner cut the value at the first space) + if ($rule === null || ($inline && !empty($rest))) { + $dropped = true; + continue; + } + $val = $inline ? $val : implode(' ', $rest); + if ($opt === 'authmech1' && $auth === null && in_array(strtoupper($val), array('PLAIN', 'LOGIN', 'CRAM-MD5'), true)) { + $auth = strtoupper($val); + } elseif ($opt === 'authmech1' || trim($val) === '' + || (is_string($rule) && preg_match('~^(?:' . str_replace('~', '\~', $rule) . ')$~D', $val) !== 1)) { + $dropped = true; + } else { + $pairs[] = array('o' => $opt, 'v' => $val); + } + } + // Old runner ignored the authmech1 column, only a custom --authmech1 was effective + $auth = $auth ?? 'PLAIN'; + $host = strtolower(trim((string)$row['host1'])); + $port = (intval($row['port1']) >= 1 && intval($row['port1']) <= 65535) ? intval($row['port1']) : 143; + $enc = in_array($row['enc1'], array('TLS', 'SSL', 'PLAIN'), true) ? $row['enc1'] : 'TLS'; + $at = strrchr((string)$row['user2'], '@'); + $domain = ($at === false) ? '' : strtolower(substr($at, 1)); + // One source per (host, port, enc, auth, domain), name always unique and <= 100 chars + $name = 'legacy-' . mb_substr($host, 0, 60) . '-' . $port . '-' . substr(sha1($host . '|' . $port . '|' . $enc . '|' . $auth . '|' . $domain), 0, 8); + $source_sel->execute(array(':name' => $name)); + if (($source_id = $source_sel->fetchColumn()) === false) { + $source_ins->execute(array(':name' => $name, ':host' => $host, ':port' => $port, ':enc' => $enc, ':auth' => $auth)); + $source_sel->execute(array(':name' => $name)); + if (($source_id = $source_sel->fetchColumn()) === false) continue; + } + if ($domain !== '') { + $source_domain_ins->execute(array(':id' => $source_id, ':domain' => $domain)); + } + $job_upd->execute(array( + ':source_id' => $source_id, + ':cp' => json_encode($pairs, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + ':cp_legacy' => $dropped ? $row['custom_params'] : null, + ':dry' => $dry ? 1 : 0, + ':id' => $row['id'] + )); + } + + // Never throw on leftovers: keep source_id nullable and the legacy columns, retried with the next db_version + $pending = intval($pdo->query("SELECT COUNT(*) FROM `imapsync` WHERE `source_id` IS NULL")->fetchColumn()); + if ($pending > 0) { + $properties['cols']['source_id'] = "INT DEFAULT NULL"; + $legacy_col_types = array( + 'host1' => "VARCHAR(255) DEFAULT NULL", + 'port1' => "SMALLINT UNSIGNED DEFAULT NULL", + 'enc1' => "ENUM('TLS','SSL','PLAIN') DEFAULT NULL", + 'authmech1' => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT NULL" + ); + foreach ($legacy_col_types as $col => $type) { + if (isset($imapsync_cols[$col])) $properties['cols'][$col] = $type; + } + error_log('mailcow: imapsync migration: ' . $pending . ' sync job(s) could not be assigned to a source and will not run'); + } + } + } + } + $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { $stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE `', `table_schema`, '`.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints` - WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;"); + WHERE `constraint_type` = 'FOREIGN KEY' AND `table_schema` = DATABASE() AND `table_name` = :table;"); $stmt->execute(array(':table' => $table)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while ($row = array_shift($rows)) { @@ -1349,20 +1610,29 @@ function init_db_schema() AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password') OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));"); - // Mitigate imapsync argument injection issue - $pdo->query("UPDATE `imapsync` SET `custom_params` = '' - WHERE `custom_params` LIKE '%pipemess%' - OR custom_params LIKE '%skipmess%' - OR custom_params LIKE '%delete2foldersonly%' - OR custom_params LIKE '%delete2foldersbutnot%' - OR custom_params LIKE '%regexflag%' - OR custom_params LIKE '%pipemess%' - OR custom_params LIKE '%regextrans2%' - OR custom_params LIKE '%maxlinelengthcmd%';"); + // Mask syncjob secrets in old log entries (data and attr arguments of the logged call) + $mask_paths = array(); + $unmasked = array(); + foreach (array(3, 4) as $idx) { + foreach (array('password1', 'password2', 'oauth_client_secret', 'oauth_extra_params') as $key) { + $mask_paths[] = "'$[" . $idx . "]." . $key . "'"; + // only rows still holding a value other than the string '*' (objects/arrays/numbers included), missing paths are NULL + $unmasked[] = "JSON_UNQUOTE(JSON_EXTRACT(`call`, '$[" . $idx . "]." . $key . "')) <> '*'"; + } + } + $pdo->exec("UPDATE `logs` SET `call` = JSON_REPLACE(`call`, " . implode(", '*', ", $mask_paths) . ", '*') + WHERE JSON_VALID(`call`) + AND (JSON_VALUE(`call`, '$[0]') = 'syncjob' + OR JSON_VALUE(`call`, '$[0]') LIKE 'imapsync%' + OR (JSON_VALUE(`call`, '$[0]') = 'mailbox' AND JSON_VALUE(`call`, '$[2]') = 'syncjob')) + AND (" . implode(" OR ", $unmasked) . ");"); // Migrate webauthn tfa $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); + // Syncjobs: seed global settings + $pdo->query("INSERT IGNORE INTO `imapsync_settings` (`name`, `value`) VALUES ('max_parallel', '1')"); + // Inject admin if not exists $stmt = $pdo->query("SELECT NULL FROM `admin`"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -1544,6 +1814,10 @@ function init_db_schema() } if (php_sapi_name() == "cli") { include '/web/inc/vars.inc.php'; + if (file_exists('/web/inc/vars.local.inc.php')) { + include '/web/inc/vars.local.inc.php'; + } + require_once '/web/inc/lib/vendor/autoload.php'; include '/web/inc/functions.inc.php'; include '/web/inc/functions.docker.inc.php'; // $now = new DateTime(); @@ -1561,7 +1835,7 @@ if (php_sapi_name() == "cli") { //PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", ]; $pdo = new PDO($dsn, $database_user, $database_pass, $opt); - $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';"); + $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND (TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view');"); $res = $stmt->fetch(PDO::FETCH_ASSOC); if (intval($res['OK_C']) === 2) { // Be more precise when replacing into _sogo_static_view, col orders may change diff --git a/data/web/inc/lib/composer.json b/data/web/inc/lib/composer.json index 3998065cb..5c3934fee 100644 --- a/data/web/inc/lib/composer.json +++ b/data/web/inc/lib/composer.json @@ -13,5 +13,10 @@ "stevenmaguire/oauth2-keycloak": "^4.0", "league/oauth2-client": "^2.7", "bacon/bacon-qr-code": "^2.0" + }, + "autoload": { + "psr-4": { + "Mailcow\\": "mailcow/" + } } } diff --git a/data/web/inc/lib/mailcow/Core/AccessDenied.php b/data/web/inc/lib/mailcow/Core/AccessDenied.php new file mode 100644 index 000000000..c17a69109 --- /dev/null +++ b/data/web/inc/lib/mailcow/Core/AccessDenied.php @@ -0,0 +1,16 @@ +success(...)` */ + public function success(string|array $msg, array $log): bool + { + $this->add('success', $msg, $log); + return true; + } + + /** Returns false, so callers can `return $response->danger(...)` */ + public function danger(string|array $msg, array $log): bool + { + $this->add('danger', $msg, $log); + return false; + } + + public function error(UserError $error, array $log): bool + { + return $this->danger($error->msg(), $log); + } + + /** Messages collected since the last flush() */ + public function messages(): array + { + return $this->messages; + } + + /** Appends the collected messages to $_SESSION['return'] and starts over */ + public function flush(): void + { + foreach ($this->messages as $message) { + $_SESSION['return'][] = $message; + } + $this->messages = []; + } + + private function add(string $type, string|array $msg, array $log): void + { + foreach ($log as $i => $part) { + if (!is_array($part)) { + continue; + } + foreach ($this->secretKeys as $key) { + if (isset($part[$key]) && $part[$key] !== '') { + $log[$i][$key] = '*'; + } + } + } + $this->messages[] = ['type' => $type, 'log' => $log, 'msg' => $msg]; + } +} diff --git a/data/web/inc/lib/mailcow/Core/Context.php b/data/web/inc/lib/mailcow/Core/Context.php new file mode 100644 index 000000000..58c462c59 --- /dev/null +++ b/data/web/inc/lib/mailcow/Core/Context.php @@ -0,0 +1,76 @@ +role === 'admin'; + } + + public function isDomainAdmin(): bool + { + return $this->role === 'domainadmin'; + } + + public function isUser(): bool + { + return $this->role === 'user'; + } + + public function hasAcl(string $name): bool + { + return isset($this->acl[$name]) && $this->acl[$name] == '1'; + } + + /** Bridge to the legacy hasMailboxObjectAccess() until functions.inc.php is converted */ + public function canAccessMailbox(string $mailbox): bool + { + return (bool)hasMailboxObjectAccess($this->username, $this->role, $mailbox); + } + + /** Bridge to the legacy hasDomainAccess() until functions.inc.php is converted */ + public function canAccessDomain(string $domain): bool + { + return (bool)hasDomainAccess($this->username, $this->role, $domain); + } +} diff --git a/data/web/inc/lib/mailcow/Core/Input.php b/data/web/inc/lib/mailcow/Core/Input.php new file mode 100644 index 000000000..fa6416cc4 --- /dev/null +++ b/data/web/inc/lib/mailcow/Core/Input.php @@ -0,0 +1,35 @@ + ['min_range' => $min, 'max_range' => $max]]); + return ($int === false) ? null : $int; + } +} diff --git a/data/web/inc/lib/mailcow/Core/Repository.php b/data/web/inc/lib/mailcow/Core/Repository.php new file mode 100644 index 000000000..d96d76175 --- /dev/null +++ b/data/web/inc/lib/mailcow/Core/Repository.php @@ -0,0 +1,65 @@ + 5]). + */ +class Repository +{ + public function __construct(protected PDO $pdo) + { + } + + /** Prepares and executes one statement */ + protected function query(string $sql, array $params = []): PDOStatement + { + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt; + } + + /** Inserts one row (column => value) and returns the new id */ + protected function insertRow(string $table, array $values): int + { + $columns = array_keys($values); + $this->query("INSERT INTO `$table` (`" . implode('`, `', $columns) . "`) VALUES (:" . implode(', :', $columns) . ")", $values); + return (int)$this->pdo->lastInsertId(); + } + + /** Updates the columns (column => value) of the row with the given `id` */ + protected function updateRow(string $table, int $id, array $values): void + { + $set = implode(', ', array_map(fn(string $column): string => "`$column` = :$column", array_keys($values))); + $this->query("UPDATE `$table` SET $set WHERE `id` = :id", $values + ['id' => $id]); + } + + /** + * Runs $work in a transaction and returns its result. Inside a transaction of the caller it just runs, + * so the caller keeps control over commit and rollback. + */ + protected function transaction(callable $work): mixed + { + if ($this->pdo->inTransaction()) { + return $work(); + } + $this->pdo->beginTransaction(); + try { + $result = $work(); + $this->pdo->commit(); + return $result; + } catch (Throwable $e) { + if ($this->pdo->inTransaction()) { + $this->pdo->rollBack(); + } + throw $e; + } + } +} diff --git a/data/web/inc/lib/mailcow/Core/UserError.php b/data/web/inc/lib/mailcow/Core/UserError.php new file mode 100644 index 000000000..e7a9b6636 --- /dev/null +++ b/data/web/inc/lib/mailcow/Core/UserError.php @@ -0,0 +1,28 @@ +args) ? $this->key : array_merge([$this->key], array_values($this->args)); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Controller/ImapsyncApiController.php b/data/web/inc/lib/mailcow/Imapsync/Controller/ImapsyncApiController.php new file mode 100644 index 000000000..0a68ed73e --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Controller/ImapsyncApiController.php @@ -0,0 +1,267 @@ +handle('add', 'job', $attr, + fn(array $log): bool => $this->response->success(['mailbox_modified', $this->jobs->create($attr)->user2], $log)); + } + + /** POST add/syncjob_source */ + public function addSource(array $attr): bool + { + return $this->handle('add', 'source', $attr, function (array $log) use ($attr): bool { + $source = $this->sources->create($attr); + $this->tokenFailure($source, $log); + return $this->response->success(['imapsync_source_added', $source->name], $log); + }); + } + + /** POST edit/syncjob: one message per id; `id` in $attr wins over $items (array_merge like before) */ + public function editJobs(?array $items, array $attr): bool + { + $data = array_merge(['id' => $items], $attr); + return $this->handle('edit', 'job', $data, function (array $log) use ($data): bool { + $this->jobs->requireAcl(); + return $this->batch((array)($data['id'] ?? []), $log, + fn(int $id): array => ['mailbox_modified', $this->jobs->update($id, $data)->user2]); + }); + } + + /** POST edit/syncjob/run_next (admin): false when no job was moved */ + public function runNext(?array $items): bool + { + return $this->handle('edit', 'run_next', ['id' => $items], function (array $log) use ($items): bool { + $outcome = $this->jobs->runNext(array_map('intval', array_values(array_filter((array)$items, 'is_numeric')))); + foreach ($outcome['running'] as $id) { + $this->response->danger(['imapsync_job_running', $id], $log); + } + return ($outcome['moved'] > 0) ? $this->response->success(['imapsync_run_next_done', $outcome['moved']], $log) : false; + }); + } + + /** POST edit/syncjob_source: one message per id, non-numeric ids are skipped */ + public function editSources(?array $items, array $attr): bool + { + $data = array_merge(['id' => $items], $attr); + return $this->handle('edit', 'source', $data, function (array $log) use ($data): bool { + $this->sources->requireEditor(); + return $this->batch(array_filter((array)($data['id'] ?? []), 'is_numeric'), $log, function (int $id) use ($data, $log): array { + $source = $this->sources->update($id, $data); + $this->tokenFailure($source, $log); + return ['imapsync_source_modified', $source->name]; + }); + }); + } + + /** POST edit/syncjob_source/refresh_token: the first id only */ + public function refreshSourceToken(?array $items): bool + { + return $this->handle('edit', 'refresh_token', ['id' => $items], function (array $log) use ($items): bool { + $source = $this->sources->refreshToken($this->requireId(is_array($items) ? reset($items) : null)); + return $this->response->success(['imapsync_source_token_refreshed', $source->name], $log); + }); + } + + /** POST edit/imapsync_settings (admin) */ + public function editSettings(array $attr): bool + { + return $this->handle('edit', 'settings', $attr, function (array $log) use ($attr): bool { + $this->jobs->saveSettings($attr); + return $this->response->success('max_parallel_saved', $log); + }); + } + + /** POST delete/syncjob: one message per id (with the id as requested) */ + public function deleteJobs(array $items): bool + { + return $this->handle('delete', 'job', ['id' => $items], function (array $log) use ($items): bool { + $this->jobs->requireAcl(); + return $this->batch($items, $log, function (int $id, mixed $requested): array { + $this->jobs->delete($id); + return ['deleted_syncjob', $requested]; + }); + }); + } + + /** POST delete/syncjob_source: one message per id, non-numeric ids are skipped */ + public function deleteSources(array $items): bool + { + return $this->handle('delete', 'source', ['id' => $items], function (array $log) use ($items): bool { + $this->sources->requireAcl(); + return $this->batch(array_filter($items, 'is_numeric'), $log, + fn(int $id): array => ['imapsync_source_deleted', $this->sources->delete($id)->name]); + }); + } + + /** GET syncjob_options */ + public function getOptions(): array + { + return $this->jobs->customParamOptions(); + } + + /** GET imapsync_settings (admin), also admin/system.php */ + public function getSettings(): array + { + try { + return $this->jobs->getSettings()->toArray(); + } catch (UserError) { + return []; + } + } + + /** GET syncjob_source/all */ + public function getSources(): array + { + return array_values(array_filter(array_map(fn(int $id): array => $this->getSource($id), $this->sources->visibleIds()))); + } + + /** GET syncjob_source/, also edit.php */ + public function getSource(mixed $id): array + { + $id = $this->toId($id); + $source = ($id === null) ? null : $this->sources->get($id); + return ($source === null) ? [] : $this->sources->toArray($source); + } + + /** + * GET syncjobs/all. Admins get every job (incl. orphaned ones). Others get the jobs of $mailboxes, + * the mailboxes the caller manages (json_api collects them with the legacy mailbox() functions + * until functions.mailbox.inc.php is converted), in queue order. + */ + public function getAllJobs(?string $extra, array $mailboxes): array + { + if ($this->jobs->canListAll()) { + return $this->jobRows($this->jobs->allIds(), $extra); + } + $ids = []; + foreach ($mailboxes as $mailbox) { + $ids = array_merge($ids, $this->jobs->idsForMailbox((string)$mailbox)); + } + $rows = $this->jobRows($ids, $extra); + // Queue order (C4): prio desc, never-run first, least recently run, id + usort($rows, fn(array $a, array $b): int => [$b['prio'], !empty($a['last_run']), (string)$a['last_run'], (int)$a['id']] + <=> [$a['prio'], !empty($b['last_run']), (string)$b['last_run'], (int)$b['id']]); + return $rows; + } + + /** GET syncjobs/; without a valid mailbox the caller's own jobs */ + public function getJobs(?string $mailbox, ?string $extra): array + { + return $this->jobRows($this->jobs->idsForMailbox($mailbox), $extra); + } + + /** One job (edit.php, inc/ajax/syncjob_logs.php); [] when missing or not accessible. `no_log` in $extra skips the log. */ + public function getJob(mixed $id, ?string $extra = null): array + { + $id = $this->toId($id); + $withLog = ($extra === null || !in_array('no_log', explode(',', $extra), true)); + $job = ($id === null) ? null : $this->jobs->get($id, $withLog); + return ($job === null) ? [] : $job->toArray($withLog); + } + + /** + * Account rename ($new) or delete ($new = null) from legacy account code that runs outside a transaction + * (admin, domain admin, mailbox delete): a database error becomes a `mysql_error` message. + * Inside a transaction (mailbox rename) call $module->sources->onAccountRenamed() directly instead, + * so the caller's catch can roll back. + */ + public function accountChanged(string $old, ?string $new = null): void + { + try { + if ($new === null) { + $this->sources->onAccountDeleted($old); + } else { + $this->sources->onAccountRenamed($old, $new); + } + } catch (PDOException $e) { + $this->response->danger(['mysql_error', $e->getMessage()], ['imapsync_account_changed', $old, $new]); + } + $this->response->flush(); + } + + /** The token fetched when saving a client_credentials source failed: the source is saved, report the error */ + private function tokenFailure(Source $source, array $log): void + { + if ($source->usesClientCredentials() && $source->active === 1 && $source->lastRefreshError !== null) { + $this->response->danger(['imapsync_source_token_refresh_failed', $source->name . ' (' . $source->lastRefreshError . ')'], $log); + } + } + + /** + * Runs one write call: $work gets the log entry and returns the result for process_*_return(). + * A UserError becomes a danger message, a database error stops the call with one `mysql_error` message. + */ + private function handle(string $action, string $type, mixed $data, callable $work): bool + { + $log = ['syncjob', $action, $type, $data]; + try { + $result = $work($log); + } catch (UserError $e) { + $result = $this->response->error($e, $log); + } catch (PDOException $e) { + $result = $this->response->danger(['mysql_error', $e->getMessage()], $log); + } + $this->response->flush(); + return $result; + } + + /** One message per id: the success message returned by $work(id, requested id), or the UserError */ + private function batch(array $ids, array $log, callable $work): bool + { + foreach ($ids as $id) { + try { + $this->response->success($work($this->requireId($id), $id), $log); + } catch (UserError $e) { + $this->response->error($e, $log); + } + } + return true; + } + + /** @return array[] API rows of the jobs the caller may access */ + private function jobRows(array $ids, ?string $extra): array + { + return array_values(array_filter(array_map(fn(int $id): array => $this->getJob($id, $extra), $ids))); + } + + /** Integer id from request input ("12", 12); null for anything else, also "1.5" (matches no row) */ + private function toId(mixed $id): ?int + { + return (is_numeric($id) && (float)$id == (int)(float)$id) ? (int)(float)$id : null; + } + + /** @throws AccessDenied for ids that cannot exist */ + private function requireId(mixed $id): int + { + return $this->toId($id) ?? throw new AccessDenied(); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Http/OAuthClient.php b/data/web/inc/lib/mailcow/Imapsync/Http/OAuthClient.php new file mode 100644 index 000000000..9729d6644 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Http/OAuthClient.php @@ -0,0 +1,172 @@ +pin($url, $strict) !== null; + } + + /** + * Sends one request (POST form when $post is given, otherwise GET) and returns the decoded JSON object. + * + * @throws OAuthException with a sanitised message + */ + public function request(string $url, bool $strict, ?array $post = null, array $headers = []): array + { + $pin = $this->pin($url, $strict); + if ($pin === null) { + throw new OAuthException('endpoint not allowed'); + } + $options = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_PROXY => '', + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 20, + CURLOPT_MAXFILESIZE => 1048576, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $headers), + ]; + if (!empty($pin)) { + $options[CURLOPT_RESOLVE] = $pin; + } + if ($post !== null) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = http_build_query($post); + } + [$body, $code] = $this->send($options); + if ($body === false) { + throw new OAuthException('connection failed'); + } + $data = json_decode($body, true); + $ok = ($code >= 200 && $code < 300); + if ($ok && is_array($data)) { + return $data; + } + throw new OAuthException($this->errorText($code, $ok, $data)); + } + + /** + * IPv4 and IPv6 addresses of a host. + * + * @return string[] + */ + protected function resolve(string $host): array + { + $ips = @gethostbynamel($host) ?: []; + foreach (@dns_get_record($host, DNS_AAAA) ?: [] as $record) { + $ips[] = $record['ipv6'] ?? ''; + } + return $ips; + } + + /** + * Runs curl with the given options. + * + * @return array{0: string|false, 1: int} response body (false on connection errors) and HTTP status + */ + protected function send(array $options): array + { + $ch = curl_init(); + curl_setopt_array($ch, $options); + $body = curl_exec($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return [is_string($body) ? $body : false, $code]; + } + + /** + * Applies the endpoint policy. Returns null when the URL is not allowed, otherwise the CURLOPT_RESOLVE + * pin (empty when not strict). + */ + private function pin(string $url, bool $strict): ?array + { + if (strlen($url) > self::MAX_URL_LENGTH || preg_match('/[\x00-\x20\x7f]/', $url)) { + return null; + } + $parts = parse_url($url); + if (!$parts || strtolower($parts['scheme'] ?? '') !== 'https' || isset($parts['user']) || isset($parts['pass'])) { + return null; + } + $host = strtolower($parts['host'] ?? ''); + if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) || !filter_var($url, FILTER_VALIDATE_URL)) { + return null; + } + if (!$strict) { + return []; + } + $ips = array_values(array_unique($this->resolve($host))); + if (empty($ips)) { + return null; + } + $pinned = []; + foreach ($ips as $ip) { + if (!$this->isPublic($ip)) { + return null; + } + $pinned[] = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $ip . ']' : $ip; + } + return [$host . ':' . (int)($parts['port'] ?? 443) . ':' . implode(',', $pinned)]; + } + + /** + * Public unicast address only: no private, loopback, link-local or reserved ranges, no CGNAT (100.64/10), + * benchmarking (198.18/15) or multicast/reserved (224/3); IPv6 only global unicast 2000::/3 without + * 6to4 (2002::/16), Teredo (2001::/32) and documentation (2001:db8::/32). + */ + private function isPublic(string $ip): bool + { + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return false; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $long = ip2long($ip); + return ($long & 0xFFC00000) != 0x64400000 + && ($long & 0xFFFE0000) != 0xC6120000 + && ($long & 0xE0000000) != 0xE0000000; + } + $hex = bin2hex((string)inet_pton($ip)); + return ($hex[0] === '2' || $hex[0] === '3') + && substr($hex, 0, 4) !== '2002' + && !in_array(substr($hex, 0, 8), ['20010000', '20010db8'], true); + } + + /** + * Error text that is safe to store: "HTTP " plus the provider's error / error_description. + */ + private function errorText(int $code, bool $ok, mixed $data): string + { + $text = 'HTTP ' . $code . ($ok ? ': malformed response' : ''); + $detail = []; + foreach (['error', 'error_description'] as $key) { + if (is_array($data) && isset($data[$key]) && is_scalar($data[$key]) && (string)$data[$key] !== '') { + $detail[] = (string)$data[$key]; + } + } + if (!empty($detail)) { + $text .= ': ' . implode(' - ', $detail); + } + return mb_substr(trim((string)preg_replace('/[\x00-\x1f\x7f]+/', ' ', strip_tags($text))), 0, self::MAX_ERROR_LENGTH); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Http/OAuthException.php b/data/web/inc/lib/mailcow/Imapsync/Http/OAuthException.php new file mode 100644 index 000000000..a8b5cac81 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Http/OAuthException.php @@ -0,0 +1,15 @@ +" plus the provider's + * error fields, or a fixed text; never a raw response body or curl details. + */ +class OAuthException extends RuntimeException +{ +} diff --git a/data/web/inc/lib/mailcow/Imapsync/ImapsyncModule.php b/data/web/inc/lib/mailcow/Imapsync/ImapsyncModule.php new file mode 100644 index 000000000..caa07b5cf --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/ImapsyncModule.php @@ -0,0 +1,73 @@ + CustomParams::allowlist($IMAPSYNC_OPTIONS, $IMAPSYNC_OPTIONS_DEFAULT), + * 'hostname' => (string)getenv('MAILCOW_HOSTNAME'), + * ]); + * process_add_return($imapsync->api->addJob($attr)); + * + * Procedural legacy code (json_api.php, pages, functions.*.inc.php) uses fromGlobals() instead. + */ +class ImapsyncModule +{ + /** Request keys whose values never reach a log */ + private const SECRET_KEYS = ['password1', 'password2', 'oauth_client_secret', 'oauth_extra_params']; + + public function __construct( + public readonly SourceService $sources, + public readonly JobService $jobs, + public readonly OAuthService $oauth, + public readonly ImapsyncApiController $api + ) { + } + + /** + * @param array $config 'allowlist' => effective custom param allowlist (CustomParams::allowlist()), + * 'hostname' => MAILCOW_HOSTNAME (OAuth redirect URI); both optional + */ + public static function create(PDO $pdo, Context $context, array $config = []): self + { + $sourceRepository = new SourceRepository($pdo); + $tokenRepository = new TokenRepository($pdo); + $oauth = new OAuthService($sourceRepository, $tokenRepository, new OAuthClient(), (string)($config['hostname'] ?? '')); + $sources = new SourceService($sourceRepository, $tokenRepository, $oauth, $context); + $jobs = new JobService(new JobRepository($pdo), $sources, $tokenRepository, new SettingsRepository($pdo), + $context, (array)($config['allowlist'] ?? [])); + return new self($sources, $jobs, $oauth, new ImapsyncApiController($sources, $jobs, new ApiResponse(self::SECRET_KEYS))); + } + + /** + * Legacy entry point for procedural callers: the one place that reads the globals ($pdo, $_SESSION, + * $IMAPSYNC_OPTIONS / $IMAPSYNC_OPTIONS_DEFAULT from vars(.local).inc.php, MAILCOW_HOSTNAME) + * and passes them to create(). $context overrides the session caller (e.g. Context::system()). + */ + public static function fromGlobals(?Context $context = null): self + { + return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []), [ + 'allowlist' => CustomParams::allowlist((array)($GLOBALS['IMAPSYNC_OPTIONS'] ?? []), + (array)($GLOBALS['IMAPSYNC_OPTIONS_DEFAULT'] ?? [])), + 'hostname' => (string)getenv('MAILCOW_HOSTNAME'), + ]); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Model/CustomParams.php b/data/web/inc/lib/mailcow/Imapsync/Model/CustomParams.php new file mode 100644 index 000000000..4a900e855 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Model/CustomParams.php @@ -0,0 +1,97 @@ + self::MAX_VALUE_LENGTH || count($pairs) >= self::MAX_PAIRS) { + throw new UserError('imapsync_custom_params_format'); + } + $pairs[] = ['o' => $option, 'v' => $value]; + } + return json_encode($pairs, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * The effective allowlist: $IMAPSYNC_OPTIONS (after vars.local.inc.php) without the old + * 'whitelist' key and malformed entries; the defaults when nothing valid is left. + */ + public static function allowlist(array $configured, array $defaults): array + { + $allowlist = []; + foreach ($configured as $option => $rule) { + $option = strtolower(ltrim(trim((string)$option), '-')); + if ($option !== 'whitelist' && preg_match('/^[a-z][a-z0-9]*$/', $option) + && (is_bool($rule) || (is_string($rule) && $rule !== ''))) { + $allowlist[$option] = $rule; + } + } + if (!self::$warnedOldFormat && isset($configured['whitelist'])) { + self::$warnedOldFormat = true; + error_log("mailcow: \$IMAPSYNC_OPTIONS['whitelist'] (old format) is ignored, use 'option' => true|false|'regex' (see data/web/inc/vars.inc.php)"); + } + return empty($allowlist) ? $defaults : $allowlist; + } + + /** The value to store for one option; throws when the allowlist rule is violated */ + private static function checkValue(string $option, string $value, array $allowlist): string + { + $rule = $allowlist[$option] ?? null; + if ($option === '' || !(is_bool($rule) || is_string($rule))) { + throw new UserError('imapsync_custom_param_invalid', [htmlspecialchars($option)]); + } + $empty = (trim($value) === ''); + $valid = match (true) { + $rule === false => $empty, + $rule === true => !$empty, + default => !$empty && preg_match('~^(?:' . str_replace('~', '\~', $rule) . ')$~D', $value) === 1, + }; + if (!$valid) { + throw new UserError('imapsync_custom_param_value', [$option]); + } + return ($rule === false) ? '' : $value; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Model/Job.php b/data/web/inc/lib/mailcow/Imapsync/Model/Job.php new file mode 100644 index 000000000..438049874 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Model/Job.php @@ -0,0 +1,154 @@ +` (columns of the source) + * and `user_token_*` columns. + */ + public static function fromRow(array $row): self + { + $job = new self(); + $job->id = isset($row['id']) ? (int)$row['id'] : null; + foreach (['authmd51', 'maxage', 'delete2duplicates', 'delete1', 'delete2', 'automap', 'skipcrossduplicates', 'timeout1', + 'timeout2', 'subscribeall', 'dry', 'active', 'prio'] as $column) { + $job->$column = (int)($row[$column] ?? 0); + } + foreach (['user2', 'domain2', 'subfolder2', 'user1', 'password1', 'exclude'] as $column) { + $job->$column = (string)($row[$column] ?? ''); + } + $job->minsInterval = (int)($row['mins_interval'] ?? 0); + $job->maxBytesPerSecond = (string)($row['maxbytespersecond'] ?? '0'); + $job->isRunning = (int)($row['is_running'] ?? 0); + $job->success = isset($row['success']) ? (int)$row['success'] : null; + // Nullable text columns (column => property) + foreach (['regextrans2' => 'regextrans2', 'custom_params' => 'customParams', 'custom_params_legacy' => 'customParamsLegacy', + 'returned_text' => 'returnedText', 'last_run' => 'lastRun', 'exit_status' => 'exitStatus', 'created' => 'created', + 'modified' => 'modified'] as $column => $property) { + $job->$property = isset($row[$column]) ? (string)$row[$column] : null; + } + $job->sourceId = (int)($row['source_id'] ?? 0); + if (isset($row['source_name'])) { + // source_id is the job's foreign key, i.e. the id of the source + $source = []; + foreach ($row as $key => $value) { + if (str_starts_with($key, 'source_')) { + $source[substr($key, 7)] = $value; + } + } + $job->source = Source::fromRow($source); + } + if (!empty($row['user_token_exists'])) { + $job->token = OAuthToken::fromRow(['source_id' => $job->sourceId, 'owner' => $job->user2, 'username' => $job->user1, + 'token_expires' => $row['user_token_expires'] ?? null, 'last_refresh_error' => $row['user_token_error'] ?? null]); + } + return $job; + } + + /** + * API representation (GET syncjobs, edit page, log view). Never contains password1. + * Without $withLog the `log` field is empty (UI tables request `no_log`). + */ + public function toArray(bool $withLog = true): array + { + $source = $this->source; + return [ + 'id' => $this->id, + 'user2' => $this->user2, + 'regextrans2' => $this->regextrans2, + 'authmd51' => $this->authmd51, + 'domain2' => $this->domain2, + 'subfolder2' => $this->subfolder2, + 'user1' => $this->user1, + 'exclude' => $this->exclude, + 'maxage' => $this->maxage, + 'mins_interval' => $this->minsInterval, + 'maxbytespersecond' => $this->maxBytesPerSecond, + 'delete2duplicates' => $this->delete2duplicates, + 'delete1' => $this->delete1, + 'delete2' => $this->delete2, + 'automap' => $this->automap, + 'skipcrossduplicates' => $this->skipcrossduplicates, + 'custom_params' => $this->customParams, + 'timeout1' => $this->timeout1, + 'timeout2' => $this->timeout2, + 'subscribeall' => $this->subscribeall, + 'dry' => $this->dry, + 'is_running' => $this->isRunning, + 'last_run' => $this->lastRun, + 'success' => $this->success, + 'exit_status' => $this->exitStatus, + 'created' => $this->created, + 'modified' => $this->modified, + 'active' => $this->active, + 'source_id' => $this->sourceId, + 'custom_params_legacy' => $this->customParamsLegacy, + 'prio' => $this->prio, + 'source_name' => $source?->name, + 'source_description' => $source?->description, + 'source_host' => $source?->host, + 'source_port' => $source?->port, + 'source_enc' => $source?->enc, + 'source_auth_type' => $source?->authType, + 'source_created_by' => $source?->createdBy, + 'source_scope' => $source?->scope, + 'source_oauth_flow' => $source?->oauthFlow, + 'source_active' => $source?->active, + 'source_oauth_token_expires' => $source?->tokenExpires, + 'source_oauth_last_refresh_error' => $source?->lastRefreshError, + 'user_token_expires' => $this->token?->tokenExpires, + 'user_token_error' => $this->token?->lastRefreshError, + 'user_token_exists' => $this->token === null ? 0 : 1, + 'log' => ($withLog && !empty($this->returnedText)) ? $this->returnedText : '', + ]; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Model/OAuthToken.php b/data/web/inc/lib/mailcow/Imapsync/Model/OAuthToken.php new file mode 100644 index 000000000..c0d28f80d --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Model/OAuthToken.php @@ -0,0 +1,38 @@ +sourceId = (int)($row['source_id'] ?? 0); + $token->owner = (string)($row['owner'] ?? ''); + $token->username = (string)($row['username'] ?? ''); + $token->accessToken = isset($row['access_token']) ? (string)$row['access_token'] : null; + $token->refreshToken = isset($row['refresh_token']) ? (string)$row['refresh_token'] : null; + $token->tokenExpires = isset($row['token_expires']) ? (int)$row['token_expires'] : null; + $token->lastRefreshError = isset($row['last_refresh_error']) ? (string)$row['last_refresh_error'] : null; + $token->refreshAttemptAge = isset($row['refresh_attempt_age']) ? (int)$row['refresh_attempt_age'] : null; + return $token; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Model/Settings.php b/data/web/inc/lib/mailcow/Imapsync/Model/Settings.php new file mode 100644 index 000000000..24eb4d268 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Model/Settings.php @@ -0,0 +1,27 @@ + $this->maxParallel, + 'max_bytes_per_second' => $this->maxBytesPerSecond, + 'max_kb_per_second' => intdiv($this->maxBytesPerSecond, 1024), + ]; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Model/Source.php b/data/web/inc/lib/mailcow/Imapsync/Model/Source.php new file mode 100644 index 000000000..5ea55d054 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Model/Source.php @@ -0,0 +1,143 @@ +id = isset($row['id']) ? (int)$row['id'] : null; + $source->name = (string)($row['name'] ?? ''); + $source->description = (string)($row['description'] ?? ''); + $source->createdBy = (string)($row['created_by'] ?? ''); + $source->scope = (string)($row['scope'] ?? 'user'); + $source->host = (string)($row['host1'] ?? ''); + $source->port = (int)($row['port1'] ?? 0); + $source->oauthFlow = (string)($row['oauth_flow'] ?? self::FLOW_CLIENT_CREDENTIALS); + $source->tokenExpires = isset($row['oauth_token_expires']) ? (int)$row['oauth_token_expires'] : null; + $source->refreshAttemptAge = isset($row['refresh_attempt_age']) ? (int)$row['refresh_attempt_age'] : null; + // Nullable text columns (column => property) + foreach (['enc1' => 'enc', 'auth_type' => 'authType', 'oauth_token_endpoint' => 'tokenEndpoint', + 'oauth_authorize_endpoint' => 'authorizeEndpoint', 'oauth_userinfo_endpoint' => 'userinfoEndpoint', + 'oauth_client_id' => 'clientId', 'oauth_client_secret' => 'clientSecret', 'oauth_scope' => 'oauthScope', + 'oauth_extra_params' => 'extraParams', 'oauth_access_token' => 'accessToken', 'oauth_last_refresh_error' => 'lastRefreshError', + 'oauth_last_refresh_attempt' => 'lastRefreshAttempt', 'created' => 'created', 'modified' => 'modified'] as $column => $property) { + $source->$property = isset($row[$column]) ? (string)$row[$column] : null; + } + $source->active = (int)($row['active'] ?? 1); + return $source; + } + + public function isXOAuth2(): bool + { + return $this->authType === self::AUTH_XOAUTH2; + } + + /** XOAUTH2 with one app-wide token that reaches every mailbox of the tenant */ + public function usesClientCredentials(): bool + { + return $this->isXOAuth2() && $this->oauthFlow === self::FLOW_CLIENT_CREDENTIALS; + } + + /** XOAUTH2 with a per-user token from the browser sign-in */ + public function usesAuthorizationCode(): bool + { + return $this->isXOAuth2() && $this->oauthFlow === self::FLOW_AUTHORIZATION_CODE; + } + + /** + * API representation (GET syncjob_source). Secrets and tokens are replaced by has_* flags; + * extra params are only shown to callers who may edit the source. + */ + public function toArray(bool $canEdit, string $redirectUri): array + { + $data = [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'created_by' => $this->createdBy, + 'scope' => $this->scope, + 'host1' => $this->host, + 'port1' => $this->port, + 'enc1' => $this->enc, + 'auth_type' => $this->authType, + 'oauth_flow' => $this->oauthFlow, + 'oauth_token_endpoint' => $this->tokenEndpoint, + 'oauth_authorize_endpoint' => $this->authorizeEndpoint, + 'oauth_userinfo_endpoint' => $this->userinfoEndpoint, + 'oauth_client_id' => $this->clientId, + 'oauth_scope' => $this->oauthScope, + 'oauth_extra_params' => $canEdit ? $this->extraParams : null, + 'oauth_token_expires' => $this->tokenExpires, + 'oauth_last_refresh_error' => $this->lastRefreshError, + 'oauth_last_refresh_attempt' => $this->lastRefreshAttempt, + 'created' => $this->created, + 'modified' => $this->modified, + 'active' => $this->active, + 'domains' => $this->domains, + 'users' => $this->users, + 'can_edit' => $canEdit, + 'host' => $this->host, + 'port' => $this->port, + 'enc' => $this->enc, + 'has_client_secret' => !empty($this->clientSecret), + 'has_access_token' => !empty($this->accessToken), + ]; + if ($this->usesAuthorizationCode()) { + $data['oauth_redirect_uri'] = $redirectUri; + } + return $data; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Repository/JobRepository.php b/data/web/inc/lib/mailcow/Imapsync/Repository/JobRepository.php new file mode 100644 index 000000000..04654906a --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Repository/JobRepository.php @@ -0,0 +1,128 @@ +query("SELECT i.`" . implode("`, i.`", $columns) . "`, + s.`name` AS source_name, s.`description` AS source_description, s.`host1` AS source_host1, + s.`port1` AS source_port1, s.`enc1` AS source_enc1, s.`auth_type` AS source_auth_type, + s.`created_by` AS source_created_by, s.`scope` AS source_scope, s.`oauth_flow` AS source_oauth_flow, + s.`active` AS source_active, s.`oauth_token_expires` AS source_oauth_token_expires, + s.`oauth_last_refresh_error` AS source_oauth_last_refresh_error, + t.`token_expires` AS user_token_expires, t.`last_refresh_error` AS user_token_error, + CASE WHEN t.`username` IS NULL THEN 0 ELSE 1 END AS user_token_exists + FROM `imapsync` i + LEFT JOIN `imapsync_source` s ON i.`source_id` = s.`id` + LEFT JOIN `imapsync_source_oauth_token` t + ON t.`source_id` = i.`source_id` AND t.`owner` = i.`user2` AND t.`username` = i.`user1` + WHERE i.`id` = :id", ['id' => $id])->fetch(PDO::FETCH_ASSOC); + return $row ? Job::fromRow($row) : null; + } + + /** @return int[] job ids of a local mailbox in queue order */ + public function idsForMailbox(string $user2): array + { + return array_map('intval', $this->query("SELECT `id` FROM `imapsync` WHERE `user2` = :user2 " . self::QUEUE_ORDER, + ['user2' => $user2])->fetchAll(PDO::FETCH_COLUMN)); + } + + /** @return int[] all job ids (incl. orphaned ones) in queue order */ + public function allIds(): array + { + return array_map('intval', $this->query("SELECT `id` FROM `imapsync` " . self::QUEUE_ORDER)->fetchAll(PDO::FETCH_COLUMN)); + } + + /** True when another job (not $exceptId) already syncs (source, user1) into user2 */ + public function tupleExists(int $sourceId, string $user2, string $user1, ?int $exceptId = null): bool + { + return (bool)$this->query("SELECT 1 FROM `imapsync` + WHERE `user2` = :user2 AND `user1` = :user1 AND `source_id` = :source_id AND `id` != :id", + ['user2' => $user2, 'user1' => $user1, 'source_id' => $sourceId, 'id' => $exceptId ?? 0])->fetchColumn(); + } + + public function insert(Job $job): int + { + return $job->id = $this->insertRow('imapsync', $this->values($job) + ['user2' => $job->user2]); + } + + public function update(Job $job): void + { + $this->updateRow('imapsync', (int)$job->id, + $this->values($job) + ['last_run' => $job->lastRun, 'success' => $job->success, 'prio' => $job->prio]); + } + + public function delete(int $id): void + { + $this->query("DELETE FROM `imapsync` WHERE `id` = :id", ['id' => $id]); + } + + /** + * Moves idle jobs to the front of the queue: each gets a prio above the current global max + * (lowest current prio first) and last_run/success are cleared. The rows are locked first, so the + * runner cannot claim a job in between (its claim UPDATE waits for this transaction). + * + * @param int[] $ids + * @return array{running: int[], moved: int} ids that are running (left alone) and the number of moved jobs + */ + public function moveToFront(array $ids): array + { + return $this->transaction(function () use ($ids): array { + $rows = $this->query("SELECT `id`, `is_running` FROM `imapsync` WHERE `id` IN (" . self::placeholders($ids) . ") FOR UPDATE", + $ids)->fetchAll(PDO::FETCH_KEY_PAIR); + $running = array_keys(array_filter($rows, fn($isRunning): bool => (int)$isRunning === 1)); + $idle = array_values(array_diff(array_keys($rows), $running)); + $moved = 0; + if (!empty($idle)) { + $this->pdo->exec("SET @m := (SELECT COALESCE(MAX(`prio`), 0) FROM `imapsync`)"); + $this->pdo->exec("SET @r := 0"); + $moved = $this->query("UPDATE `imapsync` SET `prio` = @m + (@r := @r + 1), `last_run` = NULL, `success` = NULL + WHERE `id` IN (" . self::placeholders($idle) . ") AND `is_running` = 0 ORDER BY `prio` ASC, `id` ASC", $idle)->rowCount(); + } + return ['running' => $running, 'moved' => $moved]; + }); + } + + /** Values of the columns a user can edit (shared by insert and update) */ + private function values(Job $job): array + { + $values = ['mins_interval' => $job->minsInterval, 'maxbytespersecond' => $job->maxBytesPerSecond, + 'custom_params' => $job->customParams, 'source_id' => $job->sourceId]; + // Columns with a property of the same name + foreach (['user1', 'password1', 'subfolder2', 'exclude', 'maxage', 'timeout1', 'timeout2', 'active', 'delete1', 'delete2', + 'delete2duplicates', 'automap', 'skipcrossduplicates', 'subscribeall', 'dry'] as $column) { + $values[$column] = $job->$column; + } + return $values; + } + + private static function placeholders(array $values): string + { + return implode(',', array_fill(0, count($values), '?')); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Repository/SettingsRepository.php b/data/web/inc/lib/mailcow/Imapsync/Repository/SettingsRepository.php new file mode 100644 index 000000000..6f96e512e --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Repository/SettingsRepository.php @@ -0,0 +1,32 @@ + value). + */ +class SettingsRepository extends Repository +{ + /** Stored settings; keys that are not stored yet keep the defaults of the model */ + public function load(): Settings + { + $rows = $this->query("SELECT `name`, `value` FROM `imapsync_settings`")->fetchAll(PDO::FETCH_KEY_PAIR); + $settings = new Settings(); + $settings->maxParallel = (int)($rows['max_parallel'] ?? $settings->maxParallel); + $settings->maxBytesPerSecond = (int)($rows['max_bytes_per_second'] ?? $settings->maxBytesPerSecond); + return $settings; + } + + public function save(Settings $settings): void + { + $sql = "INSERT INTO `imapsync_settings` (`name`, `value`) VALUES (:name, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)"; + $this->query($sql, ['name' => 'max_parallel', 'value' => (string)$settings->maxParallel]); + $this->query($sql, ['name' => 'max_bytes_per_second', 'value' => (string)$settings->maxBytesPerSecond]); + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Repository/SourceRepository.php b/data/web/inc/lib/mailcow/Imapsync/Repository/SourceRepository.php new file mode 100644 index 000000000..b35397013 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Repository/SourceRepository.php @@ -0,0 +1,215 @@ +fetch('1=1', ['id' => $id]); + } + + /** Like find(), but only when the source is visible to the caller */ + public function findVisible(int $id, Context $context): ?Source + { + [$visible, $params] = $this->visibility($context); + return $this->fetch($visible, $params + ['id' => $id]); + } + + /** @return int[] ids of all sources visible to the caller, ordered by name */ + public function visibleIds(Context $context): array + { + [$visible, $params] = $this->visibility($context); + return array_map('intval', $this->query("SELECT s.`id` FROM `imapsync_source` s WHERE $visible ORDER BY s.`name`, s.`id`", + $params)->fetchAll(PDO::FETCH_COLUMN)); + } + + public function nameExists(string $name, string $createdBy, ?int $exceptId = null): bool + { + return (bool)$this->query("SELECT 1 FROM `imapsync_source` WHERE `name` = :name AND `created_by` = :created_by AND `id` != :id", + ['name' => $name, 'created_by' => $createdBy, 'id' => $exceptId ?? 0])->fetchColumn(); + } + + /** True while a sync job references the source */ + public function isInUse(int $id): bool + { + return (bool)$this->query("SELECT 1 FROM `imapsync` WHERE `source_id` = :id LIMIT 1", ['id' => $id])->fetchColumn(); + } + + public function isSuperAdmin(string $username): bool + { + return (bool)$this->query("SELECT 1 FROM `admin` WHERE `username` = :username AND `superadmin` = '1'", + ['username' => $username])->fetchColumn(); + } + + /** Inserts the source with its targets and returns the new id */ + public function insert(Source $source): int + { + return $this->transaction(function () use ($source): int { + $source->id = $this->insertRow('imapsync_source', + $this->values($source) + ['created_by' => $source->createdBy, 'scope' => $source->scope]); + $this->writeTargets($source); + return $source->id; + }); + } + + /** + * Updates the source. $writeTargets also rewrites scope + target lists, $resetTokens drops the shared + * token and all per-user tokens of the source. All in one transaction. + */ + public function update(Source $source, bool $writeTargets, bool $resetTokens): void + { + $this->transaction(function () use ($source, $writeTargets, $resetTokens): void { + $id = ['id' => $source->id]; + $this->updateRow('imapsync_source', (int)$source->id, $this->values($source)); + if ($writeTargets) { + $this->query("DELETE FROM `imapsync_source_domain` WHERE `source_id` = :id", $id); + $this->query("DELETE FROM `imapsync_source_user` WHERE `source_id` = :id", $id); + $this->writeTargets($source); + } + if ($resetTokens) { + $this->query("UPDATE `imapsync_source` SET `oauth_access_token` = NULL, `oauth_token_expires` = NULL, + `oauth_last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NULL WHERE `id` = :id", $id); + $this->query("DELETE FROM `imapsync_source_oauth_token` WHERE `source_id` = :id", $id); + } + }); + } + + /** Deletes the source; targets and per-user tokens go via FK ON DELETE CASCADE */ + public function delete(int $id): void + { + $this->query("DELETE FROM `imapsync_source` WHERE `id` = :id", ['id' => $id]); + } + + /** Deletes the source and its child rows explicitly, for callers running with FOREIGN_KEY_CHECKS = 0 (mailbox rename) */ + public function deleteWithChildren(int $id): void + { + foreach (['imapsync_source_oauth_token', 'imapsync_source_user', 'imapsync_source_domain'] as $table) { + $this->query("DELETE FROM `$table` WHERE `source_id` = :id", ['id' => $id]); + } + $this->delete($id); + } + + /** @return Source[] sources created by an account (id and name only) */ + public function listCreatedBy(string $username): array + { + return array_map([Source::class, 'fromRow'], $this->query("SELECT `id`, `name` FROM `imapsync_source` WHERE `created_by` = :username", + ['username' => $username])->fetchAll(PDO::FETCH_ASSOC)); + } + + /** Hands a source over to the admins (created_by '') under the given name */ + public function handOver(int $id, string $name): void + { + $this->query("UPDATE `imapsync_source` SET `created_by` = '', `name` = :name WHERE `id` = :id", ['name' => $name, 'id' => $id]); + } + + /** Follows an account rename: creator and user targets */ + public function renameAccount(string $old, string $new): void + { + $this->query("UPDATE `imapsync_source` SET `created_by` = :new WHERE `created_by` = :old", ['new' => $new, 'old' => $old]); + $this->query("UPDATE `imapsync_source_user` SET `username` = :new WHERE `username` = :old", ['new' => $new, 'old' => $old]); + } + + /** Removes a deleted account from all user target lists */ + public function deleteUserTarget(string $username): void + { + $this->query("DELETE FROM `imapsync_source_user` WHERE `username` = :username", ['username' => $username]); + } + + private function fetch(string $where, array $params): ?Source + { + $row = $this->query("SELECT s.* FROM `imapsync_source` s WHERE s.`id` = :id AND $where", $params)->fetch(PDO::FETCH_ASSOC); + if (!$row) { + return null; + } + $source = Source::fromRow($row); + $id = ['id' => $source->id]; + $source->domains = $this->query("SELECT `domain` FROM `imapsync_source_domain` WHERE `source_id` = :id ORDER BY `domain`", + $id)->fetchAll(PDO::FETCH_COLUMN); + $source->users = $this->query("SELECT `username` FROM `imapsync_source_user` WHERE `source_id` = :id ORDER BY `username`", + $id)->fetchAll(PDO::FETCH_COLUMN); + return $source; + } + + /** + * WHERE fragment over `imapsync_source s` that limits rows to what the caller may see, and its parameters. + * admin everything + * domainadmin own + 'all' + sources targeting his domains or their mailboxes + sources created by them + * user own + 'all' + sources targeting him or his domain + * + * @return array{0: string, 1: array} + */ + private function visibility(Context $context): array + { + $username = $context->username; + if ($context->isAdmin()) { + return ['1=1', []]; + } + // Native prepares: a placeholder may appear only once, so the username is bound under one name per use + $managed = "SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = "; + if ($context->isDomainAdmin()) { + return ["(s.`created_by` = :vis_self OR s.`scope` = 'all' + OR (s.`scope` = 'domain' AND EXISTS (SELECT 1 FROM `imapsync_source_domain` t + WHERE t.`source_id` = s.`id` AND t.`domain` IN ($managed :vis_d1))) + OR (s.`scope` = 'user' AND EXISTS (SELECT 1 FROM `imapsync_source_user` t JOIN `mailbox` mb ON mb.`username` = t.`username` + WHERE t.`source_id` = s.`id` AND mb.`domain` IN ($managed :vis_d2))) + OR s.`created_by` IN (SELECT `username` FROM `mailbox` WHERE `domain` IN ($managed :vis_d3)))", + ['vis_self' => $username, 'vis_d1' => $username, 'vis_d2' => $username, 'vis_d3' => $username]]; + } + if ($context->isUser()) { + return ["(s.`created_by` = :vis_self OR s.`scope` = 'all' + OR (s.`scope` = 'user' AND EXISTS (SELECT 1 FROM `imapsync_source_user` t + WHERE t.`source_id` = s.`id` AND t.`username` = :vis_u1)) + OR (s.`scope` = 'domain' AND EXISTS (SELECT 1 FROM `imapsync_source_domain` t JOIN `mailbox` mb ON mb.`domain` = t.`domain` + WHERE t.`source_id` = s.`id` AND mb.`username` = :vis_u2)))", + ['vis_self' => $username, 'vis_u1' => $username, 'vis_u2' => $username]]; + } + return ['0=1', []]; + } + + private function writeTargets(Source $source): void + { + foreach ($source->domains as $domain) { + $this->query("INSERT INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)", + ['id' => $source->id, 'domain' => $domain]); + } + foreach ($source->users as $username) { + $this->query("INSERT INTO `imapsync_source_user` (`source_id`, `username`) VALUES (:id, :username)", + ['id' => $source->id, 'username' => $username]); + } + $this->query("UPDATE `imapsync_source` SET `scope` = :scope WHERE `id` = :id", ['scope' => $source->scope, 'id' => $source->id]); + } + + private function values(Source $source): array + { + return [ + 'name' => $source->name, + 'description' => $source->description, + 'host1' => $source->host, + 'port1' => $source->port, + 'enc1' => $source->enc, + 'auth_type' => $source->authType, + 'oauth_flow' => $source->oauthFlow, + 'oauth_token_endpoint' => $source->tokenEndpoint, + 'oauth_authorize_endpoint' => $source->authorizeEndpoint, + 'oauth_userinfo_endpoint' => $source->userinfoEndpoint, + 'oauth_client_id' => $source->clientId, + 'oauth_client_secret' => $source->clientSecret, + 'oauth_scope' => $source->oauthScope, + 'oauth_extra_params' => $source->extraParams, + 'active' => $source->active, + ]; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Repository/TokenRepository.php b/data/web/inc/lib/mailcow/Imapsync/Repository/TokenRepository.php new file mode 100644 index 000000000..69499b525 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Repository/TokenRepository.php @@ -0,0 +1,123 @@ +query("SELECT 1 FROM `imapsync_source_oauth_token` WHERE " . self::USER_TOKEN, + ['sid' => $sourceId, 'owner' => $owner, 'user' => $username])->fetchColumn(); + } + + /** + * Stores the token of a finished browser sign-in. An existing refresh token is kept when the + * provider sent none (Microsoft only sends one with offline_access). + */ + public function saveUserToken(OAuthToken $token): void + { + $this->query("INSERT INTO `imapsync_source_oauth_token` + (`source_id`, `owner`, `username`, `access_token`, `refresh_token`, `token_expires`, `last_refresh_error`, `oauth_last_refresh_attempt`) + VALUES (:sid, :owner, :user, :acc, :ref, :exp, NULL, NOW()) + ON DUPLICATE KEY UPDATE + `access_token` = VALUES(`access_token`), + `refresh_token` = IF(VALUES(`refresh_token`) = '', `refresh_token`, VALUES(`refresh_token`)), + `token_expires` = VALUES(`token_expires`), + `last_refresh_error` = NULL, + `oauth_last_refresh_attempt` = NOW()", + $this->key($token) + ['acc' => $token->accessToken, 'ref' => (string)$token->refreshToken, 'exp' => $token->tokenExpires]); + } + + /** Stores a refreshed per-user token and clears its last error */ + public function storeUserRefresh(OAuthToken $token): void + { + $this->query("UPDATE `imapsync_source_oauth_token` SET `access_token` = :acc, `refresh_token` = :ref, + `token_expires` = :exp, `last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NOW() WHERE " . self::USER_TOKEN, + $this->key($token) + ['acc' => $token->accessToken, 'ref' => $token->refreshToken, 'exp' => $token->tokenExpires]); + } + + public function storeUserError(OAuthToken $token, string $error): void + { + $this->query("UPDATE `imapsync_source_oauth_token` SET `last_refresh_error` = :err, `oauth_last_refresh_attempt` = NOW() + WHERE " . self::USER_TOKEN, $this->key($token) + ['err' => $error]); + } + + public function storeSourceToken(int $sourceId, string $accessToken, int $expires): void + { + $this->query("UPDATE `imapsync_source` SET `oauth_access_token` = :tok, `oauth_token_expires` = :exp, + `oauth_last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NOW() WHERE `id` = :id", + ['tok' => $accessToken, 'exp' => $expires, 'id' => $sourceId]); + } + + public function storeSourceError(int $sourceId, string $error): void + { + $this->query("UPDATE `imapsync_source` SET `oauth_last_refresh_error` = :err, `oauth_last_refresh_attempt` = NOW() + WHERE `id` = :id", ['err' => $error, 'id' => $sourceId]); + } + + /** @return Source[] active client_credentials sources (incl. secrets), each with refreshAttemptAge */ + public function refreshableSources(): array + { + return array_map([Source::class, 'fromRow'], $this->query("SELECT *, + TIMESTAMPDIFF(SECOND, `oauth_last_refresh_attempt`, NOW()) AS `refresh_attempt_age` + FROM `imapsync_source` + WHERE `active` = 1 AND `auth_type` = 'XOAUTH2' AND `oauth_flow` = 'client_credentials' + ORDER BY `id`")->fetchAll(PDO::FETCH_ASSOC)); + } + + /** @return OAuthToken[] per-user tokens with a refresh token on active authorization_code sources, with refreshAttemptAge */ + public function refreshableUserTokens(): array + { + return array_map([OAuthToken::class, 'fromRow'], $this->query("SELECT t.*, + TIMESTAMPDIFF(SECOND, t.`oauth_last_refresh_attempt`, NOW()) AS `refresh_attempt_age` + FROM `imapsync_source_oauth_token` t + JOIN `imapsync_source` s ON s.`id` = t.`source_id` + WHERE s.`active` = 1 AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code' + AND t.`refresh_token` <> '' + ORDER BY t.`source_id`, t.`owner`, t.`username`")->fetchAll(PDO::FETCH_ASSOC)); + } + + /** Drops a per-user token once no job uses (source, owner, username) anymore */ + public function deleteIfUnused(int $sourceId, string $owner, string $username): void + { + $this->query("DELETE FROM `imapsync_source_oauth_token` WHERE " . self::USER_TOKEN . " + AND NOT EXISTS (SELECT 1 FROM `imapsync` WHERE `source_id` = :sid2 AND `user2` = :owner2 AND `user1` = :user2)", + ['sid' => $sourceId, 'owner' => $owner, 'user' => $username, 'sid2' => $sourceId, 'owner2' => $owner, 'user2' => $username]); + } + + /** Drops per-user tokens that no job uses and that are older than $seconds. Returns the number of rows. */ + public function deleteUnusedOlderThan(int $seconds): int + { + return $this->query("DELETE t FROM `imapsync_source_oauth_token` t + LEFT JOIN `imapsync` i ON i.`source_id` = t.`source_id` AND i.`user2` = t.`owner` AND i.`user1` = t.`username` + WHERE i.`id` IS NULL AND t.`created` < NOW() - INTERVAL :seconds SECOND", ['seconds' => $seconds])->rowCount(); + } + + public function deleteByOwner(string $owner): void + { + $this->query("DELETE FROM `imapsync_source_oauth_token` WHERE `owner` = :owner", ['owner' => $owner]); + } + + public function renameOwner(string $old, string $new): void + { + $this->query("UPDATE `imapsync_source_oauth_token` SET `owner` = :new WHERE `owner` = :old", ['new' => $new, 'old' => $old]); + } + + private function key(OAuthToken $token): array + { + return ['sid' => $token->sourceId, 'owner' => $token->owner, 'user' => $token->username]; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Service/JobService.php b/data/web/inc/lib/mailcow/Imapsync/Service/JobService.php new file mode 100644 index 000000000..c87cac02c --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Service/JobService.php @@ -0,0 +1,286 @@ +context->hasAcl('syncjobs')) { + throw new AccessDenied(); + } + } + + /** The allowed custom params (option => rule), for the job form */ + public function customParamOptions(): array + { + return $this->allowlist; + } + + /** + * The job when the caller may access its mailbox (incl. password1: never pass the object to the UI, use toArray()). + * Admins may also manage orphaned jobs whose local mailbox no longer exists. + */ + public function get(int $id, bool $withLog = true): ?Job + { + $job = $this->jobs->find($id, $withLog); + return ($job !== null && ($this->context->isAdmin() || $this->context->canAccessMailbox($job->user2))) ? $job : null; + } + + /** @return int[] job ids of a mailbox (default: the caller's own) in queue order; [] without access */ + public function idsForMailbox(mixed $mailbox): array + { + if ($mailbox !== null && filter_var($mailbox, FILTER_VALIDATE_EMAIL)) { + if (!$this->context->canAccessMailbox((string)$mailbox)) { + return []; + } + } else { + $mailbox = $this->context->username; + } + return ($mailbox === null) ? [] : $this->jobs->idsForMailbox((string)$mailbox); + } + + /** Only admins list all jobs at once (incl. orphaned ones) */ + public function canListAll(): bool + { + return $this->context->isAdmin(); + } + + /** @return int[] all job ids in queue order; [] for non-admins */ + public function allIds(): array + { + return $this->canListAll() ? $this->jobs->allIds() : []; + } + + /** @throws UserError */ + public function create(array $input): Job + { + $this->requireAcl(); + $job = new Job(); + $job->user2 = $this->ownerMailbox($input); + // Add keeps its legacy parsing of these attributes (e.g. maxage '1.5' means none, on edit 1); then the edit rules apply + $input['source_id'] = (int)($input['source_id'] ?? 0); + $input['maxage'] = Input::intInRange($input['maxage'] ?? null, 1, 32000) ?? 0; + $input['maxbytespersecond'] = Input::intInRange($input['maxbytespersecond'] ?? null, 1, 125000000) ?? 0; + $this->save($job, null, $input + ['custom_params' => '']); + return $job; + } + + /** @throws UserError */ + public function update(int $id, array $input): Job + { + $this->requireAcl(); + $current = $this->get($id, false) ?? throw new AccessDenied(); + $job = clone $current; + $this->save($job, $current, $input); + return $job; + } + + /** @throws UserError access_denied when the job does not exist or the caller may not access it */ + public function delete(int $id): void + { + $this->requireAcl(); + $job = $this->get($id, false) ?? throw new AccessDenied(); + // prio is a sparse sort key (ties allowed), so no gap needs closing + $this->jobs->delete($id); + $this->tokens->deleteIfUnused($job->sourceId, $job->user2, $job->user1); + } + + /** + * Admin only (C4): moves jobs to the front of the queue. Running jobs are left alone; the runner never resets prio. + * + * @param int[] $ids + * @return array{running: int[], moved: int} + */ + public function runNext(array $ids): array + { + $ids = array_values(array_unique($ids)); + if (!$this->context->isAdmin() || empty($ids)) { + throw new AccessDenied(); + } + return $this->jobs->moveToFront($ids); + } + + /** @throws AccessDenied for non-admins */ + public function getSettings(): Settings + { + if (!$this->context->isAdmin()) { + throw new AccessDenied(); + } + return $this->settings->load(); + } + + /** + * Saves the global settings (admin only). The UI sends the bandwidth limit in KB/s; stored as bytes/s + * like the per-job limit. + * + * @throws UserError + */ + public function saveSettings(array $input): Settings + { + if (!$this->context->isAdmin()) { + throw new AccessDenied(); + } + $maxParallel = Input::intInRange($input['max_parallel'] ?? null, 1, 32); + $kb = $input['max_kb_per_second'] ?? 0; + if ($maxParallel === null) { + throw new UserError('imapsync_max_parallel_invalid'); + } + // 0 (unlimited) .. 10 GiB/s + $kb = Input::intInRange(($kb === '') ? 0 : $kb, 0, 10485760) ?? throw new UserError('imapsync_max_kb_per_second_invalid'); + $settings = new Settings(); + $settings->maxParallel = $maxParallel; + $settings->maxBytesPerSecond = $kb * 1024; + $this->settings->save($settings); + return $settings; + } + + /** + * Applies an add ($current = null) or edit request to $job and stores it. Attributes missing in $input keep + * the value of $job, an empty password keeps the stored one. On edit the source is only checked when the job + * moves to another source, so a domain admin or user can still edit or deactivate a job bound to a source + * he cannot see. + * + * @throws UserError + */ + private function save(Job $job, ?Job $current, array $input): void + { + foreach (self::FLAGS as $flag) { + if (isset($input[$flag])) { + $job->$flag = (int)$input[$flag]; + } + } + foreach (['maxage', 'timeout1', 'timeout2'] as $number) { + if (isset($input[$number]) && $input[$number] != '') { + $job->$number = (int)$input[$number]; + } + } + if (isset($input['maxbytespersecond']) && $input['maxbytespersecond'] != '') { + $job->maxBytesPerSecond = (string)(int)$input['maxbytespersecond']; + } + // last_run/success are reset when present; prio is a global-queue concern that only admins may change + $job->lastRun = isset($input['last_run']) ? null : $job->lastRun; + $job->success = isset($input['success']) ? null : $job->success; + if (isset($input['prio']) && is_numeric($input['prio']) && $this->context->isAdmin()) { + $job->prio = (int)$input['prio']; + } + // An empty user1 keeps the stored one on edit; a new job needs one + $user1 = (isset($input['user1']) && Input::text($input['user1']) !== '') ? Input::text($input['user1']) : $job->user1; + if ($user1 === null || mb_strlen($user1) > 255 || ($current === null && $user1 === '')) { + throw new UserError('imapsync_user1_invalid'); + } + $password = (isset($input['password1']) && is_scalar($input['password1']) && (string)$input['password1'] !== '') + ? (string)$input['password1'] : $job->password1; + $sourceId = (!empty($input['source_id']) && is_scalar($input['source_id'])) ? (int)$input['source_id'] : $job->sourceId; + $sourceChanged = ($current === null || $sourceId !== $current->sourceId); + $tupleChanged = $sourceChanged || $user1 !== $current->user1; + $job->user1 = $user1; + $job->sourceId = $sourceId; + + $source = $sourceChanged ? $this->sources->get($sourceId) : $current->source; + if ($source === null || ($sourceChanged && $source->active !== 1)) { + throw new UserError('imapsync_source_unavailable'); + } + if ($source->usesClientCredentials() && $tupleChanged && !$sourceChanged && !$this->mayUse($source)) { + // The app token reaches every mailbox of the tenant, so only callers that may use the source + // may pick another remote mailbox (user1). Other fields stay editable. + throw new AccessDenied(); + } + $job->password1 = $this->checkAuth($source, $job, $password, $tupleChanged); + if (array_key_exists('custom_params', $input)) { + $job->customParams = CustomParams::toJson($input['custom_params'], $this->allowlist); + } + $job->maxage = Input::intInRange($job->maxage, 1, 32000) ?? 0; + $job->timeout1 = Input::intInRange($job->timeout1, 1, 32000) ?? 600; + $job->timeout2 = Input::intInRange($job->timeout2, 1, 32000) ?? 600; + $job->maxBytesPerSecond = (string)(Input::intInRange($job->maxBytesPerSecond, 1, 125000000) ?? 0); + $exclude = isset($input['exclude']) ? Input::text($input['exclude']) : $job->exclude; + $subfolder2 = isset($input['subfolder2']) ? Input::text($input['subfolder2']) : $job->subfolder2; + $minsInterval = Input::intInRange(!empty($input['mins_interval']) ? $input['mins_interval'] : $job->minsInterval, 1, 43800); + // exclude must be a valid regex, the interval 1..43800 minutes + if ($exclude === null || $subfolder2 === null || $minsInterval === null || @preg_match("/" . $exclude . "/", '') === false) { + throw new AccessDenied(); + } + $job->exclude = $exclude; + $job->subfolder2 = $subfolder2; + $job->minsInterval = $minsInterval; + if ($tupleChanged && $this->jobs->tupleExists($job->sourceId, $job->user2, $job->user1, $current?->id)) { + throw new UserError('object_exists', [htmlspecialchars($source->name . ' / ' . $job->user1)]); + } + if ($current === null) { + $this->jobs->insert($job); + return; + } + $this->jobs->update($job); + // The previous (source, owner, remote user) token is dropped once no job uses it (C1) + if ($tupleChanged) { + $this->tokens->deleteIfUnused($current->sourceId, $current->user2, $current->user1); + } + } + + /** Whether the caller may use the source for new jobs (sees it or may edit it) */ + private function mayUse(Source $source): bool + { + return $this->sources->get((int)$source->id) !== null || $this->sources->canEdit($source); + } + + /** + * Local mailbox of a new job: `username` (needs access), else the caller's own mailbox (users only). + * + * @throws UserError access_denied, no_user_defined + */ + private function ownerMailbox(array $input): string + { + $username = $input['username'] ?? null; + if (is_string($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) { + return $this->context->canAccessMailbox($username) ? $username : throw new AccessDenied(); + } + return $this->context->isUser() ? (string)$this->context->username : throw new UserError('no_user_defined'); + } + + /** + * Returns the password to store. XOAUTH2 sources carry no password (the runner uses an access token); + * an authorization_code source needs the per-user token of (source, owner, remote user) when it is new. + * + * @throws UserError imapsync_source_oauth_connect_required, password_empty + */ + private function checkAuth(Source $source, Job $job, string $password, bool $tupleChanged): string + { + if (!$source->isXOAuth2()) { + return ($password !== '') ? $password : throw new UserError('password_empty'); + } + if ($source->usesAuthorizationCode() && $tupleChanged && !$this->tokens->exists($job->sourceId, $job->user2, $job->user1)) { + throw new UserError('imapsync_source_oauth_connect_required'); + } + return ''; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Service/OAuthService.php b/data/web/inc/lib/mailcow/Imapsync/Service/OAuthService.php new file mode 100644 index 000000000..5f28fa277 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Service/OAuthService.php @@ -0,0 +1,332 @@ += 15 min left) */ + private const REFRESH_AHEAD = 1200; + /** A token whose last refresh failed is retried after this many seconds */ + private const RETRY_AFTER = 180; + /** Extra params that belong to the browser sign-in; all others go to the token request */ + private const AUTHORIZE_PARAMS = ['prompt', 'access_type', 'login_hint', 'domain_hint', 'hd']; + /** Per-user tokens that no job uses are kept this long (time to save the job after signing in) */ + private const UNUSED_TOKEN_GRACE = 86400; + + private string $hostname; + + public function __construct(private SourceRepository $sources, private TokenRepository $tokens, private OAuthClient $client, + string $hostname) + { + $this->hostname = strtolower(trim($hostname)); + } + + /** + * Redirect URI to register at the provider. Only MAILCOW_HOSTNAME is supported: the popup callback + * needs the session cookie and opener origin of the UI, so the UI must be used on that host. + */ + public function redirectUri(): string + { + return ($this->hostname === '') ? '' : 'https://' . $this->hostname . '/syncjob-oauth'; + } + + /** + * Sets the OAuth settings of a source from request attributes: cleared for password sources, read and + * validated for XOAUTH2 (incl. the endpoint policy C5). Missing attributes keep the value of $source, + * an empty client secret keeps the stored one. Permission checks are the caller's job. + * + * @throws UserError + */ + public function applySettings(Source $source, array $input): void + { + if (!$source->isXOAuth2()) { + $source->oauthFlow = Source::FLOW_CLIENT_CREDENTIALS; + $source->tokenEndpoint = $source->authorizeEndpoint = $source->userinfoEndpoint = null; + $source->clientId = $source->clientSecret = $source->oauthScope = $source->extraParams = null; + return; + } + $source->oauthFlow = (($input['oauth_flow'] ?? $source->oauthFlow) === Source::FLOW_AUTHORIZATION_CODE) + ? Source::FLOW_AUTHORIZATION_CODE : Source::FLOW_CLIENT_CREDENTIALS; + $source->tokenEndpoint = $this->oauthText($input, 'oauth_token_endpoint', $source->tokenEndpoint); + $authorizeEndpoint = $this->oauthText($input, 'oauth_authorize_endpoint', $source->authorizeEndpoint); + $userinfoEndpoint = $this->oauthText($input, 'oauth_userinfo_endpoint', $source->userinfoEndpoint); + $source->clientId = $this->oauthText($input, 'oauth_client_id', $source->clientId); + $source->oauthScope = $this->oauthText($input, 'oauth_scope', $source->oauthScope); + $secret = $input['oauth_client_secret'] ?? ''; + if (!is_scalar($secret)) { + throw new UserError('imapsync_source_oauth_invalid'); + } + $source->clientSecret = ((string)$secret !== '') ? (string)$secret : (string)$source->clientSecret; + $source->extraParams = $this->extraParams(array_key_exists('oauth_extra_params', $input) ? $input['oauth_extra_params'] : $source->extraParams); + + if ($source->tokenEndpoint === '' || $source->clientId === '' || $source->clientSecret === '' || $source->oauthScope === '') { + throw new UserError('imapsync_source_oauth_required'); + } + if (mb_strlen($source->clientId) > 500 || mb_strlen($source->clientSecret) > 500 || mb_strlen($source->oauthScope) > 500) { + throw new UserError('imapsync_source_oauth_invalid'); + } + // C5, checked again before every request. The authorize endpoint is only opened in the browser, + // but must not point users to internal hosts either. + $endpoints = [$source->tokenEndpoint]; + if ($source->usesAuthorizationCode()) { + $endpoints[] = $authorizeEndpoint; + } + if ($userinfoEndpoint !== '') { + $endpoints[] = $userinfoEndpoint; + } + foreach ($endpoints as $endpoint) { + if (!$this->endpointAllowed($endpoint, $source->createdBy)) { + throw new UserError('imapsync_source_oauth_endpoint_invalid'); + } + } + $source->authorizeEndpoint = ($authorizeEndpoint === '') ? null : $authorizeEndpoint; + $source->userinfoEndpoint = ($userinfoEndpoint === '') ? null : $userinfoEndpoint; + } + + /** Provider consent URL for the browser sign-in, or null when the authorize endpoint is not allowed */ + public function authorizeUrl(Source $source, string $state): ?string + { + $endpoint = (string)$source->authorizeEndpoint; + if (!$this->endpointAllowed($endpoint, $source->createdBy)) { + return null; + } + // select_account: never sign in silently with the account the browser already uses (often the admin's) + $params = array_merge(['prompt' => 'select_account'], $this->extraParamsFor($source, true), ['client_id' => $source->clientId, + 'response_type' => 'code', 'redirect_uri' => $this->redirectUri(), 'response_mode' => 'query', + 'scope' => $source->oauthScope, 'state' => $state]); + return $endpoint . (str_contains($endpoint, '?') ? '&' : '?') . http_build_query($params); + } + + /** + * Finishes the browser sign-in: exchanges the code, finds the remote identity and stores the token + * for the local mailbox $owner (C1). Returns the remote identity (the job's user1). + * + * @throws UserError token_failed (args: detail for the server log) or no_identity + */ + public function connectUser(Source $source, string $owner, string $code): string + { + try { + $token = $this->requestToken($source, ['grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => $this->redirectUri()]); + } catch (OAuthException $e) { + throw new UserError('token_failed', ['token exchange failed for source ' . $source->id . ': ' . $e->getMessage()]); + } + $identity = $this->identity($source, $token); + if ($identity === null) { + throw new UserError('no_identity'); + } + try { + $this->tokens->saveUserToken(OAuthToken::fromRow(['source_id' => $source->id, 'owner' => $owner, 'username' => $identity, + 'access_token' => $token['access_token'], 'refresh_token' => $token['refresh_token'] ?? '', + 'token_expires' => time() + (int)$token['expires_in']])); + } catch (PDOException $e) { + throw new UserError('token_failed', ['storing token failed for source ' . $source->id . ': ' . $e->getMessage()]); + } + return $identity; + } + + /** Fetches and stores a new shared token of a client_credentials source (or stores the error); returns the error or null */ + public function refreshSourceToken(Source $source): ?string + { + if (!$source->usesClientCredentials()) { + return 'not refreshable'; + } + try { + $token = $this->requestToken($source, ['grant_type' => 'client_credentials']); + } catch (OAuthException $e) { + $this->tokens->storeSourceError((int)$source->id, $e->getMessage()); + return $e->getMessage(); + } + $this->tokens->storeSourceToken((int)$source->id, $token['access_token'], time() + (int)$token['expires_in']); + return null; + } + + /** Refreshes and stores a per-user token via its refresh token (or stores the error); returns the error or null */ + public function refreshUserToken(Source $source, OAuthToken $token): ?string + { + if (!$source->usesAuthorizationCode() || (string)$token->refreshToken === '') { + return 'not refreshable'; + } + try { + $new = $this->requestToken($source, ['grant_type' => 'refresh_token', 'refresh_token' => $token->refreshToken]); + } catch (OAuthException $e) { + $this->tokens->storeUserError($token, $e->getMessage()); + return $e->getMessage(); + } + $token->accessToken = $new['access_token']; + // Some providers omit a new refresh token on refresh: keep the existing one then + $token->refreshToken = empty($new['refresh_token']) ? $token->refreshToken : $new['refresh_token']; + $token->tokenExpires = time() + (int)$new['expires_in']; + $this->tokens->storeUserRefresh($token); + return null; + } + + /** + * Cron run: refreshes due source tokens, drops unused per-user tokens, refreshes due per-user tokens. + * + * @return array log entries for CRON_LOG + */ + public function refreshDue(): array + { + $log = []; + foreach ($this->tokens->refreshableSources() as $source) { + if ($this->isDue($source->tokenExpires, $source->lastRefreshError, $source->refreshAttemptAge)) { + try { + $error = $this->refreshSourceToken($source); + } catch (Exception $e) { + $error = get_class($e); + } + $label = "source '" . $source->name . "' (id=" . $source->id . ")"; + $log[] = $this->logEntry($error, "Refreshed token for " . $label, "Refresh failed for " . $label); + } + } + $removed = $this->tokens->deleteUnusedOlderThan(self::UNUSED_TOKEN_GRACE); + if ($removed > 0) { + $log[] = ['priority' => 'info', 'message' => "Removed " . $removed . " unused user token(s)"]; + } + foreach ($this->tokens->refreshableUserTokens() as $token) { + if ($this->isDue($token->tokenExpires, $token->lastRefreshError, $token->refreshAttemptAge)) { + $source = null; + try { + $source = $this->sources->find($token->sourceId); + $error = ($source === null) ? 'not refreshable' : $this->refreshUserToken($source, $token); + } catch (Exception $e) { + $error = get_class($e); + } + $label = "user token '" . $token->username . "' (mailbox " . $token->owner . ") on source '" + . ($source->name ?? '') . "' (id=" . $token->sourceId . ")"; + $log[] = $this->logEntry($error, "Refreshed " . $label, "Refresh failed for " . $label); + } + } + return $log; + } + + /** Due when the token expires soon (or is missing) and the last failed attempt is old enough */ + private function isDue(?int $expires, ?string $lastError, ?int $attemptAge): bool + { + return ($expires === null || $expires < time() + self::REFRESH_AHEAD) + && ($lastError === null || $attemptAge === null || $attemptAge > self::RETRY_AFTER); + } + + private function logEntry(?string $error, string $success, string $failure): array + { + return ($error === null) ? ['priority' => 'info', 'message' => $success] + : ['priority' => 'warning', 'message' => $failure . ": " . $error]; + } + + /** + * Endpoint policy (C5). Endpoints of sources not created by an admin must resolve to public addresses; + * admin sources have no address restriction (API key sessions run as admin under the reserved name 'API'). + */ + private function endpointAllowed(string $url, string $createdBy): bool + { + return $this->client->isAllowed($url, $this->isStrict($createdBy)); + } + + private function isStrict(string $createdBy): bool + { + return !($createdBy === 'API' || $this->sources->isSuperAdmin($createdBy)); + } + + /** One OAuth text attribute from the request, or the stored value when the attribute is missing */ + private function oauthText(array $input, string $key, ?string $stored): string + { + $value = array_key_exists($key, $input) ? Input::text($input[$key]) : (string)$stored; + return $value ?? throw new UserError('imapsync_source_oauth_invalid'); + } + + /** Stored extra params of the browser sign-in ($authorize) or of the token request */ + private function extraParamsFor(Source $source, bool $authorize): array + { + $extra = json_decode((string)$source->extraParams, true); + return array_filter(is_array($extra) ? $extra : [], + fn($key): bool => in_array($key, self::AUTHORIZE_PARAMS, true) === $authorize, ARRAY_FILTER_USE_KEY); + } + + /** + * Extra parameters: a JSON object with scalar values, as string or as array (after 011-api.js + * auto-parsing). Returns the JSON to store, null for none. + */ + private function extraParams(mixed $extra): ?string + { + if ($extra === null || (is_string($extra) && trim($extra) === '')) { + $extra = []; + } elseif (is_string($extra)) { + $extra = json_decode($extra, true); + } + $bad = !is_array($extra); + foreach ((array)$extra as $key => $value) { + $bad = $bad || !is_string($key) || $key === '' || !(is_scalar($value) || $value === null); + } + $json = empty($extra) ? null : json_encode($extra, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + if ($bad || strlen((string)$json) > 4000) { + throw new UserError('imapsync_source_oauth_extra_invalid'); + } + return $json; + } + + /** + * Token endpoint request for every grant type. Stored extra params never override the client and grant fields. + * + * @throws OAuthException + */ + private function requestToken(Source $source, array $grant): array + { + $post = array_merge($this->extraParamsFor($source, false), + ['client_id' => $source->clientId, 'client_secret' => $source->clientSecret, 'scope' => $source->oauthScope], $grant); + $token = $this->client->request((string)$source->tokenEndpoint, $this->isStrict($source->createdBy), $post); + if (empty($token['access_token']) || !is_string($token['access_token'])) { + throw new OAuthException('malformed token response'); + } + if (!isset($token['expires_in']) || !is_numeric($token['expires_in'])) { + $token['expires_in'] = 3600; + } + if (isset($token['refresh_token']) && !is_string($token['refresh_token'])) { + unset($token['refresh_token']); + } + return $token; + } + + /** Remote identity: id_token claims (received directly from the token endpoint over TLS), else the userinfo endpoint */ + private function identity(Source $source, array $token): ?string + { + $parts = explode('.', is_string($token['id_token'] ?? null) ? $token['id_token'] : ''); + $email = (count($parts) === 3) ? $this->emailClaim(json_decode((string)base64_decode(strtr($parts[1], '-_', '+/')), true)) : null; + if ($email !== null || empty($source->userinfoEndpoint)) { + return $email; + } + try { + return $this->emailClaim($this->client->request($source->userinfoEndpoint, $this->isStrict($source->createdBy), null, + ['Authorization: Bearer ' . $token['access_token']])); + } catch (OAuthException $e) { + return null; + } + } + + private function emailClaim(mixed $claims): ?string + { + foreach (['email', 'preferred_username', 'upn', 'unique_name'] as $key) { + if (is_array($claims) && !empty($claims[$key]) && is_string($claims[$key]) && filter_var($claims[$key], FILTER_VALIDATE_EMAIL)) { + return strtolower($claims[$key]); + } + } + return null; + } +} diff --git a/data/web/inc/lib/mailcow/Imapsync/Service/SourceService.php b/data/web/inc/lib/mailcow/Imapsync/Service/SourceService.php new file mode 100644 index 000000000..b3b70a8b0 --- /dev/null +++ b/data/web/inc/lib/mailcow/Imapsync/Service/SourceService.php @@ -0,0 +1,357 @@ +context->hasAcl('syncjobs')) { + throw new AccessDenied(); + } + } + + /** Add/edit: a logged-in admin, domain admin or user with the syncjobs ACL */ + public function requireEditor(): void + { + if (!($this->context->isAdmin() || $this->context->isDomainAdmin() || $this->context->isUser())) { + throw new AccessDenied(); + } + $this->requireAcl(); + } + + /** + * The source when the caller may see it (incl. secrets: never pass the object to the UI, use toArray()). + * Users and domain admins without the syncjobs ACL see no sources. + */ + public function get(int $id): ?Source + { + return $this->canRead() ? $this->repo->findVisible($id, $this->context) : null; + } + + /** @return int[] ids of the sources the caller may see, ordered by name */ + public function visibleIds(): array + { + return $this->canRead() ? $this->repo->visibleIds($this->context) : []; + } + + public function canEdit(Source $source): bool + { + if ($this->context->accessAll || $this->context->isAdmin() || $source->createdBy === $this->context->username) { + return true; + } + // A domain admin may manage sources created by mailboxes of his domains + return $this->context->isDomainAdmin() && $source->createdBy !== '' && $this->context->canAccessMailbox($source->createdBy); + } + + /** API representation for the caller (can_edit, extra params only for editors, redirect URI) */ + public function toArray(Source $source): array + { + return $source->toArray($this->canEdit($source), $this->oauth->redirectUri()); + } + + /** + * Creates a source from request attributes (C9 names; host1/port1/enc1 are accepted as aliases). + * + * @throws UserError + */ + public function create(array $input): Source + { + $this->requireEditor(); + $source = new Source(); + $source->createdBy = (string)$this->context->username; + $this->applyBasics($source, $input); + $this->applyOAuth($source, $input); + // Scope and targets are validated before anything is written + $this->applyScope($source, $input['scope'] ?? 'user', $input['domains'] ?? [], $input['users'] ?? []); + if ($this->repo->nameExists($source->name, $source->createdBy)) { + throw new UserError('object_exists', [htmlspecialchars($source->name)]); + } + $this->repo->insert($source); + $this->fetchToken($source); + return $source; + } + + /** + * Updates a source; attributes missing in $input keep their value. + * + * @throws UserError + */ + public function update(int $id, array $input): Source + { + $this->requireEditor(); + $current = $this->editable($id); + $source = clone $current; + $this->applyBasics($source, $input); + if ($source->name !== $current->name && $this->repo->nameExists($source->name, $source->createdBy, $id)) { + throw new UserError('object_exists', [htmlspecialchars($source->name)]); + } + $this->applyOAuth($source, $input); + // Targets only change when the request carries `scope`; client_credentials sources are always private + $writeTargets = isset($input['scope']) || $source->usesClientCredentials(); + if ($writeTargets) { + $this->applyScope($source, $input['scope'] ?? $current->scope, $input['domains'] ?? [], $input['users'] ?? []); + } + $this->repo->update($source, $writeTargets, self::connection($current) !== self::connection($source)); + $this->fetchToken($source); + return $source; + } + + /** + * Deletes an unused source and returns it. + * + * @throws UserError access_denied, imapsync_source_in_use + */ + public function delete(int $id): Source + { + $this->requireAcl(); + $source = $this->editable($id); + // FK ON DELETE RESTRICT prevents removal while sync jobs reference it + if ($this->repo->isInUse($id)) { + throw new UserError('imapsync_source_in_use', [$source->name]); + } + $this->repo->delete($id); + return $source; + } + + /** + * Fetches a new shared token for a client_credentials source now. + * + * @throws UserError access_denied, imapsync_source_refresh_unsupported, imapsync_source_token_refresh_failed + */ + public function refreshToken(int $id): Source + { + $this->requireAcl(); + $source = $this->editable($id); + if (!$source->usesClientCredentials()) { + throw new UserError('imapsync_source_refresh_unsupported'); + } + $error = $this->oauth->refreshSourceToken($source); + if ($error !== null) { + throw new UserError('imapsync_source_token_refresh_failed', [$source->name . ' (' . $error . ')']); + } + return $source; + } + + /** A visible, active authorization_code source the caller may sign in to (OAuth popup), else null */ + public function findConnectable(int $id): ?Source + { + $source = $this->get($id); + return ($source !== null && $source->active === 1 && $source->usesAuthorizationCode()) ? $source : null; + } + + /** + * Account rename (mailbox, admin, domain admin): sources, user targets and token owners follow. + * Leftover sources of an earlier account named $new are released first (see releaseSourcesOf()). + * Runs on the caller's PDO; a PDOException is not caught, so a caller transaction can roll back. + */ + public function onAccountRenamed(string $old, string $new): void + { + if ($old === '' || $old === $new) { + return; + } + $this->releaseSourcesOf($new); + $this->repo->renameAccount($old, $new); + $this->tokens->renameOwner($old, $new); + } + + /** Account deleted (after its jobs were deleted): sources released, tokens and user targets removed */ + public function onAccountDeleted(string $old): void + { + if ($old === '') { + return; + } + $this->releaseSourcesOf($old); + $this->tokens->deleteByOwner($old); + $this->repo->deleteUserTarget($old); + } + + private function canRead(): bool + { + return $this->context->hasAcl('syncjobs') || $this->context->isAdmin(); + } + + /** @throws AccessDenied when the caller cannot see or edit the source */ + private function editable(int $id): Source + { + $source = $this->get($id); + if ($source === null || !$this->canEdit($source)) { + throw new AccessDenied(); + } + return $source; + } + + /** + * Sources of an account are deleted when unused, otherwise handed over to the admins + * (created_by ''), so a re-created account inherits nothing. + */ + private function releaseSourcesOf(string $username): void + { + foreach ($this->repo->listCreatedBy($username) as $source) { + $id = (int)$source->id; + if (!$this->repo->isInUse($id)) { + // Explicit child deletes: the mailbox rename runs with FOREIGN_KEY_CHECKS = 0 (no cascade) + $this->repo->deleteWithChildren($id); + continue; + } + // Keep (created_by, name) unique when handing over + $name = $this->repo->nameExists($source->name, '') ? mb_substr($source->name, 0, 80) . ' (#' . $id . ')' : $source->name; + $this->repo->handOver($id, $name); + } + } + + /** + * Name, description, connection and active from the request; missing attributes keep the value of $source. + * On edit, stored host/port/enc that do not change are not validated again, so migrated legacy sources + * stay editable (e.g. host 'imap_server.lan'). A host that only differs in case keeps the stored spelling. + * + * @throws UserError + */ + private function applyBasics(Source $source, array $input): void + { + $stored = ($source->id !== null); + $name = isset($input['name']) ? Input::text($input['name']) : $source->name; + $description = isset($input['description']) ? Input::text($input['description']) : $source->description; + $host = $input['host'] ?? $input['host1'] ?? null; + $host = ($host === null) ? $source->host : Input::text($host); + if ($host !== null) { + $host = (strtolower($host) === strtolower($source->host)) ? $source->host : strtolower($host); + } + $port = $input['port'] ?? $input['port1'] ?? $source->port; + $port = is_scalar($port) ? (int)$port : 0; + $enc = $input['enc'] ?? $input['enc1'] ?? $source->enc; + $authType = $input['auth_type'] ?? $source->authType; + + if ($name === null || $name === '' || mb_strlen($name) > 100) { + throw new UserError('imapsync_source_name_invalid'); + } + if ($description === null || mb_strlen($description) > 255) { + throw new UserError('imapsync_source_description_invalid'); + } + if (!($stored && $host === $source->host) && !$this->isValidHost($host)) { + throw new UserError('imapsync_source_host_invalid'); + } + if (!($stored && $port === $source->port) && ($port < 1 || $port > 65535)) { + throw new UserError('imapsync_source_port_invalid'); + } + if (!($stored && $enc === $source->enc) && !in_array($enc, self::ENCRYPTIONS, true)) { + throw new UserError('imapsync_source_enc_invalid'); + } + if (!in_array($authType, self::AUTH_TYPES, true)) { + throw new UserError('imapsync_source_auth_invalid'); + } + $source->name = $name; + $source->description = $description; + $source->host = $host; + $source->port = $port; + $source->enc = $enc; + $source->authType = $authType; + $source->active = isset($input['active']) ? (int)$input['active'] : $source->active; + } + + private function isValidHost(?string $host): bool + { + return $host !== null && $host !== '' && strlen($host) <= 253 + && (filter_var($host, FILTER_VALIDATE_IP) || filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)); + } + + /** @throws UserError */ + private function applyOAuth(Source $source, array $input): void + { + // XOAUTH2 sources are managed by admins and domain admins only + if ($source->isXOAuth2() && $this->context->isUser()) { + throw new AccessDenied(); + } + $this->oauth->applySettings($source, $input); + } + + /** + * Validates scope and targets for the caller and sets them; nothing is written here. + * Users' sources and client_credentials sources (one app-wide token that reaches every mailbox) are always private. + * + * @throws UserError access_denied, imapsync_source_scope_empty + */ + private function applyScope(Source $source, mixed $scope, mixed $domains, mixed $users): void + { + if ($source->usesClientCredentials() || $this->context->isUser()) { + [$source->scope, $source->domains, $source->users] = ['user', [], []]; + return; + } + // Only admins may create globally visible sources + if (!is_string($scope) || !in_array($scope, self::SCOPES, true) || ($scope === 'all' && !$this->context->isAdmin())) { + throw new AccessDenied(); + } + $domains = ($scope === 'domain') ? $this->cleanList($domains) : []; + $users = ($scope === 'user') ? $this->cleanList($users) : []; + if ($scope === 'domain' && empty($domains)) { + throw new UserError('imapsync_source_scope_empty'); + } + foreach ($domains as $domain) { + if (!$this->context->canAccessDomain($domain)) { + throw new AccessDenied(); + } + } + foreach ($users as $user) { + if (!$this->context->canAccessMailbox($user)) { + throw new AccessDenied(); + } + } + [$source->scope, $source->domains, $source->users] = [$scope, $domains, $users]; + } + + /** @return string[] trimmed, lowercase, unique, non-empty scalar values */ + private function cleanList(mixed $list): array + { + $clean = []; + foreach ((array)$list as $value) { + if (is_scalar($value) && trim((string)$value) !== '') { + $clean[] = strtolower(trim((string)$value)); + } + } + return array_values(array_unique($clean)); + } + + /** A saved, active client_credentials source gets its token now instead of at the next cron run (error in lastRefreshError) */ + private function fetchToken(Source $source): void + { + if ($source->usesClientCredentials() && $source->active === 1) { + $source->lastRefreshError = $this->oauth->refreshSourceToken($source); + } + } + + /** Tokens never follow a source to another OAuth client or IMAP server, so these values reset them */ + private static function connection(Source $source): array + { + return [(string)$source->authType, $source->host, $source->port, (string)$source->enc, $source->oauthFlow, + (string)$source->tokenEndpoint, (string)$source->clientId]; + } +} diff --git a/data/web/inc/lib/mailcow/README.md b/data/web/inc/lib/mailcow/README.md new file mode 100644 index 000000000..f66eff55f --- /dev/null +++ b/data/web/inc/lib/mailcow/README.md @@ -0,0 +1,97 @@ +# mailcow PHP classes (`Mailcow\`) + +New backend code of the mailcow UI/API lives here as plain PHP classes, one directory per module. +It replaces the procedural `data/web/inc/functions.*.inc.php` files step by step. There is no framework, +no DI container and no ORM: just small classes, constructor injection and PDO. + +The directory sits below `inc/lib/`, which nginx never serves. Classes are loaded by Composer's PSR-4 +autoloader (`"Mailcow\\": "mailcow/"` in `inc/lib/composer.json`), which every entry point already +includes (`prerequisites.inc.php`, crons, `mailcowauth.php`). + +## Layout + +``` +Core/ shared by all modules + Context.php who is calling: role, username, ACL, access-all; fromSession(), system() + UserError.php expected error with a lang key (+ args), shown as a danger message + AccessDenied.php UserError 'access_denied' + ApiResponse.php collects messages, writes $_SESSION['return'] (the only class that does) + Input.php helpers for loosely typed request values + Repository.php base class of all repositories: PDO, query(), insertRow(), updateRow(), transaction() +/ + Module.php composition root: create(PDO, Context, array $config) wires everything; + fromGlobals() is the legacy entry point for procedural callers + Model/ plain typed objects (fromRow(), toArray() for the API shape), no DB + Repository/ SQL only (prepared statements); takes and returns models + Service/ use cases, business rules, permissions (Context), transactions via repositories + Http/ outbound I/O (e.g. OAuth requests) + Controller/ request in, messages out: adapter for json_api.php and pages +``` + +Example: `Imapsync/` (sync jobs, sync sources, OAuth). Use it as the template for new modules. + +## Layers and rules + +- **Model:** typed public properties, `fromRow()` from a DB row, `toArray()` for the API format (keys and + order are the API contract). Secrets stay in the model but never in `toArray()`. No getters/setters. +- **Repository:** extends `Core\Repository`; the only place with SQL (prepared statements via `query()`). + No permission decisions (a visibility filter that is pure SQL may take the `Context`). A method that writes + several tables wraps them in `transaction()`, which joins a transaction the caller already runs. +- **Service:** one use case per public method, working on single objects. Checks permissions with the + `Context` and throws `UserError` / `AccessDenied` with a lang key. No `$_SESSION`, no `global`, no output. + When add and edit share their rules, use one private routine for both (e.g. `JobService::save()`, + `SourceService::applyBasics()`): missing attributes keep the current value, a new object starts with defaults. +- **Controller:** normalises request input (batch ids, attribute merging), loops over batch ids, calls + services, turns `UserError` into messages via `ApiResponse` and returns what `json_api.php` passes to + `process_*_return()`. The API format (routes, JSON, message keys and order) stays as it was. Each write + method is a few lines on top of two private helpers: `handle()` (log entry, error mapping, flush) and + `batch()` (one message per id). +- **View:** Twig templates, unchanged. + +## Conventions + +- `declare(strict_types=1);` in every file; typed properties, parameters and return types; constructor + property promotion; `public readonly` properties instead of getters for values that never change. +- Class files contain only declarations (no code at file level), one class per file. +- Constructor injection. Global state (`$pdo`, `$_SESSION`, config arrays, `getenv()`) is only read at the + entry points and passed to `Module::create()`. Procedural legacy callers (`json_api.php`, pages, + `functions.*.inc.php`) use `Module::fromGlobals(?Context $context = null)`, the one documented + place in the module that reads the globals, instead of repeating the config lines; entry points that build + their own state (crons, CLI) call `create()` directly, e.g. with `Context::system()`. + `ApiResponse::flush()` is the one class that writes `$_SESSION['return']`. +- No interfaces until there are two implementations; no closures as injected dependencies (local callbacks + such as `transaction(fn ...)` or the controller's `handle()` are fine); small methods. +- Docblocks only where they add information (rules, API contract, non-obvious behaviour), not to repeat + the signature. +- Classes are not `final`, so PHPUnit can mock them. I/O that tests must replace sits in protected methods + (see `Imapsync/Http/OAuthClient::send()`). +- Messages: services throw, controllers report. The `msg` is a lang key of the matching section + (`success` / `danger`), optionally with arguments: `new UserError('object_exists', [$name])`. +- Logs: the `log` part of a message is `[, , , ]` as before; request + keys holding secrets are masked by `ApiResponse` (configure them in the module). +- A database error (`PDOException`) stops the call with one `mysql_error` message, like the legacy code. +- Naming: `Mailcow\\{Model,Repository,Service,Http,Controller}\`. + +## Converting a legacy `functions.X.inc.php` + +1. Capture the current API behaviour (E2E runs, raw JSON responses, DB state). +2. Build the module as above: models, repositories, services, controller, `Module::create()`. +3. Switch the callers (`json_api.php`, pages, crons, other function files) to the module. +4. Delete the function file and its `require` lines. +5. Compare the API output with step 1; the format must stay the same. + +Large files (e.g. `functions.mailbox.inc.php`) are converted one area at a time (aliases, domains, ...), +each as its own module. Until `functions.inc.php` is converted, `Context::canAccessMailbox()` / +`canAccessDomain()` call the legacy `hasMailboxObjectAccess()` / `hasDomainAccess()`. + +## Development + +- Unit tests: `tests/php//...` (dev-only, never deployed), run with + `helper-scripts/dev_tests/run_php_unit_tests.sh`. Services are tested with mocked repositories; + repositories are covered by the E2E suites (MariaDB-specific SQL). +- Autoloader: after adding the PSR-4 mapping nothing needs regenerating for new classes. If you ever run + `composer dump-autoload` (inside the php-fpm container, `-w /web/inc/lib`), never use `-o`, `-a` or + `--apcu`, and commit only the changed `autoload_*.php` files. +- No dev packages in `inc/lib/composer.json`. +- opcache runs with `validate_timestamps=0`: restart php-fpm after changing PHP files + (`docker compose restart php-fpm-mailcow`). diff --git a/data/web/inc/lib/vendor/composer/autoload_psr4.php b/data/web/inc/lib/vendor/composer/autoload_psr4.php index 236034cf0..afc9a1dd1 100644 --- a/data/web/inc/lib/vendor/composer/autoload_psr4.php +++ b/data/web/inc/lib/vendor/composer/autoload_psr4.php @@ -25,6 +25,7 @@ return array( 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), 'MatthiasMullie\\PathConverter\\' => array($vendorDir . '/matthiasmullie/path-converter/src'), 'MatthiasMullie\\Minify\\' => array($vendorDir . '/matthiasmullie/minify/src'), + 'Mailcow\\' => array($baseDir . '/mailcow'), 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'), 'LdapRecord\\' => array($vendorDir . '/directorytree/ldaprecord/src'), 'Illuminate\\Support\\' => array($vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/conditionable'), diff --git a/data/web/inc/lib/vendor/composer/autoload_static.php b/data/web/inc/lib/vendor/composer/autoload_static.php index f66ad0c71..f0a06b7cf 100644 --- a/data/web/inc/lib/vendor/composer/autoload_static.php +++ b/data/web/inc/lib/vendor/composer/autoload_static.php @@ -58,6 +58,7 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b array ( 'MatthiasMullie\\PathConverter\\' => 29, 'MatthiasMullie\\Minify\\' => 22, + 'Mailcow\\' => 8, ), 'L' => array ( @@ -177,6 +178,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b array ( 0 => __DIR__ . '/..' . '/matthiasmullie/minify/src', ), + 'Mailcow\\' => + array ( + 0 => __DIR__ . '/../..' . '/mailcow', + ), 'League\\OAuth2\\Client\\' => array ( 0 => __DIR__ . '/..' . '/league/oauth2-client/src', diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 72c5da67a..451704565 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -287,131 +287,98 @@ $RSPAMD_MAPS = array( ); -$IMAPSYNC_OPTIONS = array( - 'whitelist' => array( - 'abort', - 'authmd51', - 'authmd52', - 'authmech1', - 'authmech2', - 'authuser1', - 'authuser2', - 'debug', - 'debugcontent', - 'debugcrossduplicates', - 'debugflags', - 'debugfolders', - 'debugimap', - 'debugimap1', - 'debugimap2', - 'debugmemory', - 'debugssl', - 'delete1emptyfolders', - 'delete2folders', - 'disarmreadreceipts', - 'domain1', - 'domain2', - 'domino1', - 'domino2', - 'dry', - 'errorsmax', - 'exchange1', - 'exchange2', - 'exitwhenover', - 'expunge1', - 'f1f2', - 'filterbuggyflags', - 'folder', - 'folderfirst', - 'folderlast', - 'folderrec', - 'gmail1', - 'gmail2', - 'idatefromheader', - 'include', - 'inet4', - 'inet6', - 'justconnect', - 'justfolders', - 'justfoldersizes', - 'justlogin', - 'keepalive1', - 'keepalive2', - 'log', - 'logdir', - 'logfile', - 'maxbytesafter', - 'maxlinelength', - 'maxmessagespersecond', - 'maxsize', - 'maxsleep', - 'minage', - 'minsize', - 'noabletosearch', - 'noabletosearch1', - 'noabletosearch2', - 'noexpunge1', - 'noexpunge2', - 'nofoldersizesatend', - 'noid', - 'nolog', - 'nomixfolders', - 'noresyncflags', - 'nossl1', - 'nossl2', - 'nosyncacls', - 'notls1', - 'notls2', - 'nouidexpunge2', - 'nousecache', - 'oauthaccesstoken1', - 'oauthaccesstoken2', - 'oauthdirect1', - 'oauthdirect2', - 'office1', - 'office2', - 'pidfile', - 'pidfilelocking', - 'prefix1', - 'prefix2', - 'proxyauth1', - 'proxyauth2', - 'resyncflags', - 'resynclabels', - 'search', - 'search1', - 'search2', - 'sep1', - 'sep2', - 'showpasswords', - 'skipemptyfolders', - 'ssl2', - 'sslargs1', - 'sslargs2', - 'subfolder1', - 'subscribe', - 'subscribed', - 'syncacls', - 'syncduplicates', - 'syncinternaldates', - 'synclabels', - 'tests', - 'testslive', - 'testslive6', - 'tls2', - 'truncmess', - 'usecache', - 'useheader', - 'useuid' - ), - 'blacklist' => array( - 'skipmess', - 'delete2foldersonly', - 'delete2foldersbutnot', - 'regexflag', - 'regexmess', - 'pipemess', - 'regextrans2', - 'maxlinelengthcmd' - ) +// Allowed imapsync options for a sync job's custom_params (key = option name, lowercase, without dashes): +// true = option requires a non-empty value +// false = pure flag, must NOT have a value +// string = PCRE (no delimiters) the whole non-empty value must match +// Anything not listed here is rejected when a job is saved (the runner passes stored options as they are). +// Override in vars.local.inc.php per option, e.g. $IMAPSYNC_OPTIONS['someoption'] = true; or unset($IMAPSYNC_OPTIONS['debug']); +// The old format $IMAPSYNC_OPTIONS['whitelist'] (list of names) is no longer supported and ignored (logged). +// NEVER add (RCE): 'delete2foldersonly', 'delete2foldersbutnot', 'regexflag', 'regexmess', 'pipemess', 'regextrans2', 'maxlinelengthcmd', 'oauthrefreshcmd1', 'oauthrefreshcmd2' +// NEVER add options taking local file paths (e.g. 'logfile', 'logdir', 'pidfile', 'passfile1', 'sslargs1' with SSL_ca_file) +$IMAPSYNC_OPTIONS_DEFAULT = array( + 'abort' => false, + 'authmd51' => false, + 'authuser1' => true, + 'debug' => false, + 'debugcontent' => false, + 'debugcrossduplicates' => false, + 'debugflags' => false, + 'debugfolders' => false, + 'debugimap' => false, + 'debugimap1' => false, + 'debugmemory' => false, + 'debugssl' => true, + 'delete1emptyfolders' => false, + 'delete2folders' => false, + 'disarmreadreceipts' => false, + 'domain1' => true, + 'domino1' => false, + 'errorsmax' => true, + 'exchange1' => false, + 'exitwhenover' => true, + 'expunge1' => false, + 'f1f2' => true, + 'filterbuggyflags' => false, + 'folder' => true, + 'folderfirst' => true, + 'folderlast' => true, + 'folderrec' => true, + 'gmail1' => false, + 'idatefromheader' => false, + 'include' => true, + 'inet4' => false, + 'inet6' => false, + 'justconnect' => false, + 'justfolders' => false, + 'justfoldersizes' => false, + 'justlogin' => false, + 'keepalive1' => false, + 'log' => false, + 'maxbytesafter' => true, + 'maxlinelength' => true, + 'maxmessagespersecond' => true, + 'maxsize' => true, + 'maxsleep' => true, + 'minage' => true, + 'minsize' => true, + 'noabletosearch' => false, + 'noabletosearch1' => false, + 'noexpunge1' => false, + 'nofoldersizesatend' => false, + 'noid' => false, + 'nolog' => false, + 'nomixfolders' => false, + 'noresyncflags' => false, + 'nossl1' => false, + 'nosyncacls' => false, + 'notls1' => false, + 'nousecache' => false, + 'office1' => false, + 'prefix1' => true, + 'prefix2' => true, + 'proxyauth1' => false, + 'resyncflags' => false, + 'resynclabels' => false, + 'search' => true, + 'search1' => true, + 'sep1' => true, + 'sep2' => true, + 'skipemptyfolders' => false, + 'sslargs1' => '^SSL_verify_mode=[01]$', + 'subfolder1' => true, + 'subscribe' => false, + 'subscribed' => false, + 'syncacls' => false, + 'syncduplicates' => false, + 'syncinternaldates' => false, + 'synclabels' => false, + 'tests' => false, + 'testslive' => false, + 'testslive6' => false, + 'truncmess' => true, + 'usecache' => false, + 'useheader' => true, + 'useuid' => false, ); +$IMAPSYNC_OPTIONS = $IMAPSYNC_OPTIONS_DEFAULT; diff --git a/data/web/js/build/013-mailcow.js b/data/web/js/build/013-mailcow.js index 9586af33d..d9182316a 100644 --- a/data/web/js/build/013-mailcow.js +++ b/data/web/js/build/013-mailcow.js @@ -73,7 +73,8 @@ $(document).ready(function() { } // form cache - $('[data-cached-form="true"]').formcache({key: $(this).data('id')}); + // fields marked with data-formcache-skip (secrets) are never written to localStorage + $('[data-cached-form="true"]').formcache({key: $(this).data('id'), controls: ['select', 'textarea', 'input:not([data-formcache-skip])']}); // tooltips $(function () { @@ -113,6 +114,8 @@ $(document).ready(function() { } localStorage.setItem(key, tab_id); + // remember which card of the tab was opened + localStorage.setItem(key + ':collapse', $(this).attr('data-bs-target').substring(1)); }); // open last tab $('[role="tablist"]').each(function (idx, elem) { @@ -128,7 +131,10 @@ $(document).ready(function() { dropdown.find('.dropdown-menu').removeClass('show'); dropdown.find('.dropdown-toggle').attr('aria-expanded', 'false'); var tab = $('[id^="' + lastTab + '"]'); - $(tab).find('.card-body.collapse:first').collapse('show'); + var lastCollapse = localStorage.getItem(key + ':collapse'); + var $card = lastCollapse ? $(tab).find('.card-body.collapse').filter(function() { return this.id === lastCollapse; }) : $(); + if (!$card.length) $card = $(tab).find('.card-body.collapse:first'); + $card.collapse('show'); } }); })(); diff --git a/data/web/js/site/imapsync.js b/data/web/js/site/imapsync.js new file mode 100644 index 000000000..70284b392 --- /dev/null +++ b/data/web/js/site/imapsync.js @@ -0,0 +1,783 @@ +// IMAP sync jobs and sync sources (admin/domainadmin mailbox page, user page, edit page). +// Loaded via $js_minifier only on pages that show sync jobs. +var mailcowImapsync = (function($) { + 'use strict'; + + // Lang strings: lang.syncjobs (mailbox/user page) or lang_syncjobs (edit page), '' if missing + function L(key) { + var src = (typeof lang_syncjobs !== 'undefined' && lang_syncjobs) ? lang_syncjobs : + ((typeof lang !== 'undefined' && lang && lang.syncjobs) ? lang.syncjobs : {}); + return (src[key] !== undefined && src[key] !== null) ? src[key] : ''; + } + function esc(s) { + return escapeHtml(s === null || s === undefined ? '' : s); + } + // same format as "last run" (YYYY-MM-DD HH:mm:ss), independent of the browser locale + function formatDate(ts) { + if (!ts) return ''; + var d = new Date(ts * 1000); + var p = function(n) { return (n < 10 ? '0' : '') + n; }; + return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); + } + function isAdmin() { + return typeof mailcow_cc_role !== 'undefined' && mailcow_cc_role === 'admin'; + } + + // bootstrap-select: refresh an existing instance, or init with the mailcow defaults + function refreshSelect($sel) { + if (typeof $sel.selectpicker !== 'function') return; + $sel.each(function() { + var $s = $(this); + if ($s.data('selectpicker')) { + $s.selectpicker('refresh'); + } else { + $s.selectpicker({ + 'styleBase': 'btn btn-xs-lg', + 'noneSelectedText': (typeof lang_footer !== 'undefined') ? lang_footer.nothing_selected : '' + }); + } + }); + } + + // Long source labels must not overflow the modal on small screens + function injectStyle() { + if ($('#imapsync-style').length) return; + $('head').append(''); + } + + // ===== Display helpers (tables) ===== + + // Visibility label of a source row + function scopeDisplay(item) { + if (item.scope === 'all') return ' ' + esc(L('source_scope_all')); + if (item.scope === 'domain') return esc(L('source_scope_domain') + ': ' + (item.domains || []).join(', ')); + var us = item.users || []; + if (us.length === 0) return esc(item.created_by || ''); + return esc(L('source_scope_user') + ': ' + us.join(', ')); + } + + // Token badge of a client_credentials source: based on token existence/error, expiry in title + function sourceTokenBadge(item) { + if (item.oauth_last_refresh_error) { + return '' + esc(L('token_state_failed')) + ''; + } + if (item.has_access_token) { + var title = item.oauth_token_expires ? L('source_token_status') + ': ' + formatDate(item.oauth_token_expires) : ''; + return '' + esc(L('token_state_issued')) + ''; + } + return '' + esc(L('token_state_none')) + ''; + } + + // Auth column of a source row: auth type, and for XOAUTH2 the flow (+ token badge for client credentials) + function authDisplay(item) { + var out = esc(item.auth_type); + if (item.auth_type !== 'XOAUTH2') return out; + var flow = item.oauth_flow || 'client_credentials'; + out += ' (' + esc(flow === 'authorization_code' ? L('source_flow_badge_user') : L('source_flow_badge_app')) + ')'; + // authorization_code tokens live per job (local mailbox + remote user), not on the source + if (flow === 'authorization_code') return out; + return out + '
' + sourceTokenBadge(item) + '
'; + } + + // Per-job token badge for authorization_code sources ('' for everything else) + function jobTokenBadge(item) { + if (item.source_auth_type !== 'XOAUTH2' || item.source_oauth_flow !== 'authorization_code') return ''; + if (item.user_token_error) { + return '' + esc(L('token_state_failed')) + ''; + } + if (parseInt(item.user_token_exists) === 1) { + var title = item.user_token_expires ? L('source_token_status') + ': ' + formatDate(item.user_token_expires) : ''; + return '' + esc(L('token_state_issued')) + ''; + } + return '' + esc(L('token_state_pending')) + ''; + } + + // Warning badge for jobs with not migrated legacy custom parameters + function legacyBadge(item) { + if (!item.custom_params_legacy) return ''; + return '' + + ' ' + esc(L('custom_params_legacy_badge')) + ''; + } + + // Wrappable table cell content (long hosts, mailboxes, legacy source names) + function wrapCell(html) { + return '
' + html + '
'; + } + + // Server column: "remote user @ host:port", source name below, badges after + function serverDisplay(item) { + var out = esc(item.user1 || '') + ' @ ' + esc(item.source_host || '?') + ':' + esc(item.source_port || '?'); + if (item.source_name) out += '
' + esc(item.source_name) + '
'; + var badges = [jobTokenBadge(item), legacyBadge(item)].filter(Boolean); + if (badges.length) out += '
' + badges.join(' ') + '
'; + return wrapCell(out); + } + + // Prepares the shared fields of a job row (run state, last run, result) + function prepareJobRow(item) { + item.server_w_port = serverDisplay(item); + if (parseInt(item.is_running) === 1) { + item.is_running = '' + esc(L('status_running')) + ''; + } else if (parseInt(item.active) !== 1) { + item.is_running = '' + esc(L('status_inactive')) + ''; + } else { + item.is_running = '' + esc(L('status_waiting')) + ''; + } + if (!item.last_run) { + item.last_run = '' + esc(L('never_run')) + ''; + } + if (item.success == null) { + item.success = '-'; + item.exit_status = ''; + } else { + item.success = ''; + } + if (L('syncjob_' + item.exit_status)) { + item.exit_status = L('syncjob_' + item.exit_status); + } else if (item.success != '-') { + item.exit_status = L('syncjob_check_log'); + } + item.exit_status = item.success + ' ' + esc(item.exit_status); + return item; + } + + // Job tables: ignore DataTables state saved before the rework (old ID sort, column visibility) + var JOB_TABLE_STATE_VERSION = 2; + function jobTableStateSave(settings, data) { + data.imapsync_state = JOB_TABLE_STATE_VERSION; + } + function jobTableStateLoad(settings, data) { + if (!data || data.imapsync_state !== JOB_TABLE_STATE_VERSION) return false; + // column visibility depends on the role, never restore it + if (Array.isArray(data.columns)) { + data.columns.forEach(function(c) { if (c) delete c.visible; }); + } + } + + // Sync source table (mailbox + user page). canManage: session may add/edit sources at all. + function drawSourceTable(canManage) { + if ($.fn.DataTable.isDataTable('#imapsync_source_table')) { + $('#imapsync_source_table').DataTable().columns.adjust().responsive.recalc(); + return; + } + $('#imapsync_source_table').DataTable({ + responsive: true, + processing: true, + serverSide: false, + stateSave: true, + pageLength: pagination_size, + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" + + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", + language: lang_datatables, + order: [[2, 'asc']], + ajax: { + type: "GET", + url: "/api/v1/get/syncjob_source/all", + dataSrc: function(json) { + if (!Array.isArray(json)) return []; + $.each(json, function(i, item) { + item.endpoint = wrapCell(esc((item.host || item.host1) + ':' + (item.port || item.port1) + ' (' + (item.enc || item.enc1) + ')')); + item.name_display = esc(item.name); + if (item.description) item.name_display += '
' + esc(item.description) + '
'; + item.name_display = wrapCell(item.name_display); + item.auth_display = authDisplay(item); + item.visibility_display = wrapCell(scopeDisplay(item)); + if (canManage && item.can_edit) { + item.action = ''; + } else { + item.action = ' ' + esc(L('source_view')) + ''; + } + }); + return json; + }, + error: function() { + mailcow_alert_box(L('source_list_error'), 'danger'); + } + }, + columns: [ + { + // placeholder, so the responsive control does not cover the ID + title: '', + data: null, + searchable: false, + orderable: false, + defaultContent: '', + responsivePriority: 1 + }, + { title: L('id'), data: 'id', defaultContent: '', responsivePriority: 4 }, + { title: L('source_name'), data: 'name_display', defaultContent: '', responsivePriority: 2 }, + { title: L('source_scope'), data: 'visibility_display', defaultContent: '', responsivePriority: 6 }, + { title: L('source_server'), data: 'endpoint', defaultContent: '', responsivePriority: 5 }, + { title: L('source_auth_type'), data: 'auth_display', defaultContent: '', responsivePriority: 3 }, + { + title: lang.active, + data: 'active', + defaultContent: '', + responsivePriority: 7, + render: function(d) { return d == 1 ? '' : ''; } + }, + { + title: lang.action, + data: 'action', + orderable: false, + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', + responsivePriority: 3, + defaultContent: '' + } + ] + }); + } + + // ===== Source dropdown in the job modal / edit page ===== + + function setNoSourceState($sel, empty) { + $sel.closest('form').find('.imapsync-no-source').toggleClass('d-none', !empty); + } + + // Replace the options of a source dropdown (array of jQuery options, kept in order) and select keepId (if present and enabled). + // bootstrap-select 1.14 keeps a detached title option (incl. its selectedness) across empty()/refresh, + // which overrides the selected option and leaves stale list entries, so destroy and re-init instead. + function rebuildSourceSelect($sel, $options, keepId) { + if (typeof $sel.selectpicker === 'function' && $sel.data('selectpicker')) $sel.selectpicker('destroy'); + $sel.find('option').remove(); + $sel.append($options); + var $keep = (keepId !== '') ? $sel.find('option').filter(function() { + return $(this).val() === keepId && !$(this).prop('disabled'); + }).first() : $(); + $sel.find('option').prop('selected', false).removeAttr('selected'); + if ($keep.length) { + $keep.attr('selected', 'selected').prop('selected', true); + } else { + // nothing to keep: select nothing, the required check makes the user pick a source + $sel.prop('selectedIndex', -1); + } + $sel.prop('disabled', false); + refreshSelect($sel); + if ($keep.length && $sel.data('selectpicker')) $sel.selectpicker('val', keepId); + return $keep.length > 0; + } + + // (Re)build every source dropdown from the ACL-filtered API + function populateSourceSelects() { + var $selects = $('select.imapsync-source-select'); + if (!$selects.length) return; + $selects.each(function() { + var $sel = $(this); + if ($sel.val()) $sel.data('keep-id', String($sel.val())); + $sel.data('saved-options', $sel.find('option:not(.bs-title-option)').clone()); + // disable while loading instead of a transient placeholder option + $sel.prop('disabled', true); + refreshSelect($sel); + }); + $.get('/api/v1/get/syncjob_source/all').done(function(sources) { + if (!Array.isArray(sources)) sources = []; + sources.sort(function(a, b) { return String(a.name).localeCompare(String(b.name)); }); + $selects.each(function() { + var $sel = $(this); + var cur = String($sel.data('current-source-id') || ''); + var keepId = String($sel.data('keep-id') || cur); + var opts = []; + var selectable = 0; + var listed = false; + $.each(sources, function(_, src) { + var id = String(src.id); + var $opt = sourceOption(src.id, src.name, src.host || src.host1, src.port || src.port1, src.auth_type, src.oauth_flow); + if (src.active != 1) { + $opt.attr('data-subtext', $opt.attr('data-subtext') + ' · ' + L('status_inactive')); + // the backend rejects inactive sources, only the assigned one stays selectable on the edit page + if (id !== cur) $opt.prop('disabled', true); + } + if (!$opt.prop('disabled')) selectable++; + if (id === cur) listed = true; + opts.push($opt); + }); + // edit page: the assigned source stays selectable even if the viewer cannot see it + if (cur !== '' && !listed) { + var $cur = sourceOption(cur, $sel.data('current-source-name'), $sel.data('current-source-host'), + $sel.data('current-source-port'), $sel.data('current-source-auth-type'), $sel.data('current-source-oauth-flow')); + $cur.text($cur.text() + ' ' + L('source_not_visible')); + opts.unshift($cur); + selectable++; + } + rebuildSourceSelect($sel, opts, keepId); + setNoSourceState($sel, selectable === 0); + $sel.trigger('change'); + }); + }).fail(function() { + // keep what was there before (e.g. the assigned source on the edit page) + $selects.each(function() { + var $sel = $(this); + rebuildSourceSelect($sel, $sel.data('saved-options') || $(), String($sel.data('keep-id') || $sel.data('current-source-id') || '')); + $sel.trigger('change'); + }); + mailcow_alert_box(L('source_list_error'), 'danger'); + }); + } + + function sourceOption(id, name, host, port, authType, flow) { + var sub = (host || '?') + ':' + (port || '?') + ' · ' + (authType || ''); + if (authType === 'XOAUTH2') sub += ' (' + (flow === 'authorization_code' ? L('source_flow_badge_user') : L('source_flow_badge_app')) + ')'; + return $('').val(id).text(name || ('#' + id)) + .attr('data-subtext', sub) + .attr('data-auth-type', authType || '') + .attr('data-oauth-flow', flow || ''); + } + + // Toggle password / user1 / connect rows by the selected source's auth type + function applySourceSelection($sel) { + var $opt = $sel.find('option:selected'); + var $form = $sel.closest('form'); + var isOauth = ($opt.attr('data-auth-type') === 'XOAUTH2'); + var isAuthCode = isOauth && ($opt.attr('data-oauth-flow') === 'authorization_code'); + var isEdit = ($form.data('id') === 'editsyncjob'); + var $user1 = $form.find('input[name="user1"]'); + var prevSource = $sel.data('prev-source'); + var curSource = $sel.val() || ''; + // a token belongs to (source, mailbox, remote user): switching the source drops the connect result + if (prevSource !== undefined && prevSource !== curSource && $user1.prop('readonly')) { + $user1.prop('readonly', false).val(isEdit ? $user1.data('initial') || '' : ''); + setConnectStatus($form, '', ''); + } + $sel.data('prev-source', curSource); + $form.find('.password1-row').toggle(!isOauth); + // edit: an empty password keeps the stored one + $form.find('input[name="password1"]').prop('required', !isOauth && !isEdit); + $form.find('.imapsync-oauth-connect-row').toggle(isAuthCode); + $form.find('.imapsync-user1-row').toggle(!isAuthCode); + $user1.prop('required', !isAuthCode && !isEdit); + } + + // ===== OAuth connect (authorization_code) ===== + + var oauthPending = null; + + function setConnectStatus($form, type, text, busy) { + var $status = $form.find('.imapsync-oauth-connect-status'); + $status.attr('class', 'imapsync-oauth-connect-status ms-2' + (type ? ' text-' + type : '')).empty(); + if (busy) $status.append(''); + $status.append(document.createTextNode(text || '')); + } + + function clearPending() { + if (oauthPending && oauthPending.timer) clearInterval(oauthPending.timer); + oauthPending = null; + } + + // Local mailbox the token is stored for (C1) + function connectMailbox($form) { + var $mb = $form.find('[name="username"]'); + if ($mb.length) return $mb.val() || ''; + if ($form.data('mailbox')) return String($form.data('mailbox')); + return (typeof mailcow_cc_username !== 'undefined') ? mailcow_cc_username : ''; + } + + function startConnect($form) { + var sourceId = $form.find('select.imapsync-source-select').val(); + if (!sourceId) return; + var mailbox = connectMailbox($form); + if (!mailbox) { + setConnectStatus($form, 'danger', L('oauth_select_mailbox')); + return; + } + var url = '/syncjob-oauth?action=start&source_id=' + encodeURIComponent(sourceId) + '&mailbox=' + encodeURIComponent(mailbox); + var popup = window.open(url, 'syncjob_oauth', 'width=600,height=700'); + if (!popup || popup.closed || typeof popup.closed === 'undefined') { + setConnectStatus($form, 'danger', L('oauth_popup_blocked')); + return; + } + clearPending(); + var pending = { form: $form, sourceId: String(sourceId), mailbox: mailbox.toLowerCase() }; + oauthPending = pending; + setConnectStatus($form, 'muted', L('oauth_waiting'), true); + pending.timer = setInterval(function() { + if (!popup.closed) return; + clearInterval(pending.timer); + pending.timer = null; + // stays pending: a result message that arrives after the close still wins + if (oauthPending === pending) setConnectStatus($form, 'muted', L('oauth_window_closed')); + }, 500); + } + + function onOauthMessage(ev) { + if (ev.origin !== window.location.origin || !ev.data || ev.data.type !== 'syncjob_oauth') return; + var p = oauthPending; + if (!p) return; + // only the row that started the flow is updated + if (ev.data.ok && (String(ev.data.source_id) !== p.sourceId || String(ev.data.mailbox || '').toLowerCase() !== p.mailbox)) return; + clearPending(); + if (ev.data.ok) { + p.form.find('input[name="user1"]').val(ev.data.user1).prop('readonly', true); + setConnectStatus(p.form, 'success', L('syncjob_oauth_connected') + ': ' + ev.data.user1); + } else { + var code = String(ev.data.error || '').replace(/[^a-z_]/g, ''); + setConnectStatus(p.form, 'danger', L('oauth_error_' + code) || L('oauth_error_unknown')); + } + } + + // ===== Custom params editor ===== + + // Allowlist { name: true (value) | false (flag) | 'regex' (value) }, loaded once on demand + var cpOptions = null; + var cpLoading = null; + function cpEnsureOptions(cb) { + if (cpOptions !== null) { cb(); return; } + if (!cpLoading) { + cpLoading = $.get('/api/v1/get/syncjob_options').done(function(opts) { + cpOptions = (opts && typeof opts === 'object' && !Array.isArray(opts)) ? opts : {}; + }).fail(function() { + cpLoading = null; + mailcow_alert_box(L('custom_params_options_error'), 'danger'); + }); + } + cpLoading.done(function() { cb(); }); + } + + function cpApplyValueState($row) { + var name = $row.find('.imapsync-cp-opt').val() || ''; + var isFlag = (name !== '') && !!cpOptions && cpOptions[name] === false; + var $val = $row.find('.imapsync-cp-val'); + $val.prop('disabled', isFlag); + if (isFlag) $val.val(''); + $val.prop('required', (name !== '') && !isFlag); + $val.attr('placeholder', isFlag ? L('custom_param_no_value') : L('custom_param_value')); + } + + var cpRowSeq = 0; + function cpAddRow($editor, o, v) { + var n = ++cpRowSeq; + var $row = $( + '
' + + '' + + '' + + '' + + '
' + ); + // aria-label: label elements inside .input-group break its first/last child styling + $row.find('.imapsync-cp-opt').attr('aria-label', L('custom_param_option')); + $row.find('.imapsync-cp-val').attr('aria-label', L('custom_param_value')); + $row.find('.imapsync-cp-remove').attr({ 'title': L('custom_param_remove'), 'aria-label': L('custom_param_remove') }); + var $sel = $row.find('.imapsync-cp-opt'); + $sel.append($('').attr('value', '').text('— ' + L('custom_param_option') + ' —')); + Object.keys(cpOptions || {}).sort().forEach(function(name) { + $sel.append($('').attr('value', name).text('--' + name)); + }); + // keep a stored option that is no longer allowlisted visible (the backend rejects it on save) + if (o && !(cpOptions && (o in cpOptions))) { + $sel.append($('').attr('value', o).text('--' + o)); + } + $sel.val(o || ''); + $row.find('.imapsync-cp-val').val(v || ''); + $editor.find('.imapsync-cp-rows').append($row); + cpApplyValueState($row); + } + + function cpBuild($editor) { + var $hidden = $editor.siblings('.imapsync-cp-value'); + $editor.find('.imapsync-cp-rows').empty(); + var pairs = []; + try { pairs = JSON.parse($hidden.val() || '[]'); } catch (e) { pairs = []; } + if (!Array.isArray(pairs)) pairs = []; + pairs.forEach(function(p) { + if (p && typeof p === 'object') cpAddRow($editor, p.o, p.v); + }); + } + + function cpSerialize($editor) { + var out = []; + $editor.find('.imapsync-cp-row').each(function() { + var o = $.trim($(this).find('.imapsync-cp-opt').val() || '').replace(/^-+/, ''); + if (o === '') return; + out.push({ o: o, v: $(this).find('.imapsync-cp-val').val() || '' }); + }); + $editor.siblings('.imapsync-cp-value').val(JSON.stringify(out)); + } + + function cpInit($scope) { + var $editors = $scope.find('.imapsync-cp-editor'); + if (!$editors.length) return; + cpEnsureOptions(function() { + $editors.each(function() { cpBuild($(this)); }); + }); + } + + // ===== Source editor (add modal + edit page) ===== + + function sourceFormToggle($form) { + var isOauth = $form.find('select.imapsync-source-auth-type').val() === 'XOAUTH2'; + var isAuthCode = $form.find('select.imapsync-source-oauth-flow').val() === 'authorization_code'; + var isCc = isOauth && !isAuthCode; + $form.find('.imapsync-source-oauth-block').toggle(isOauth); + $form.find('.imapsync-source-authcode-block').toggle(isOauth && isAuthCode); + $form.find('.imapsync-source-cc-block').toggle(isCc); + // client credentials sources are forced private by the backend + $form.find('.imapsync-source-scope-block').toggle(!isCc); + $form.find('.imapsync-source-scope-cc-hint').toggle(isCc); + } + + function sourceScopeToggle($form) { + var scope = $form.find('select.imapsync-source-scope').val(); + $form.find('.imapsync-source-domains-row').toggle(scope === 'domain'); + $form.find('.imapsync-source-users-row').toggle(scope === 'user'); + if (scope === 'domain') fillScopeSelect($form.find('select.imapsync-source-domains'), 'domain/all', 'domain_name'); + if (scope === 'user') fillScopeSelect($form.find('select.imapsync-source-users'), 'mailbox/all', 'username'); + } + + // Add the selectable domains/mailboxes to a scope multiselect (keeps selected options) + function fillScopeSelect($sel, url, field) { + if (!$sel.length || $sel.prop('disabled') || $sel.data('filled')) return; + $sel.data('filled', true); + $.get('/api/v1/get/' + url).done(function(rows) { + var have = {}; + $sel.find('option').each(function() { have[$(this).val()] = true; }); + $.each(Array.isArray(rows) ? rows : [], function(_, r) { + var v = r[field]; + if (!v || have[v]) return; + $sel.append($('').val(v).text(v)); + }); + // refresh duplicates preselected entries in the button label, re-init instead + if (typeof $sel.selectpicker === 'function' && $sel.data('selectpicker')) { + var keep = $sel.val(); + $sel.selectpicker('destroy'); + refreshSelect($sel); + $sel.selectpicker('val', keep); + } else { + refreshSelect($sel); + } + }).fail(function() { + $sel.data('filled', false); + mailcow_alert_box(lang_danger.access_denied, 'danger'); + }); + } + + // Extra params must be a JSON object; checked before the form is posted + function extraParamsValid($form) { + var $f = $form.find('input[name="oauth_extra_params"]'); + if (!$f.length || $f.prop('disabled') || !$f.is(':visible')) return true; + var v = $.trim($f.val() || ''); + if (v === '') { $f.removeClass('inputMissingAttr'); return true; } + var ok = false; + try { + var o = JSON.parse(v); + ok = (o !== null && typeof o === 'object' && !Array.isArray(o)); + } catch (e) { ok = false; } + $f.toggleClass('inputMissingAttr', !ok); + if (!ok) mailcow_alert_box(lang_danger.imapsync_source_oauth_extra_invalid, 'danger'); + return ok; + } + + function copyField(id) { + var el = document.getElementById(id); + if (!el) return; + var done = function() { mailcow_alert_box(L('copied'), 'success'); }; + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(el.value).then(done); + } else { + el.select(); + try { document.execCommand('copy'); done(); } catch (e) {} + } + } + + // ===== Provider presets (add source modal) ===== + + // Server and OAuth settings of well-known providers; fields a preset leaves out get the defaults of applyPreset + var m365 = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/'; + var presets = { + m365_app: {host: 'outlook.office365.com', auth_type: 'XOAUTH2', oauth_token_endpoint: m365 + 'token', + oauth_scope: 'https://outlook.office365.com/.default'}, + m365_user: {host: 'outlook.office365.com', auth_type: 'XOAUTH2', oauth_flow: 'authorization_code', + oauth_authorize_endpoint: m365 + 'authorize', oauth_token_endpoint: m365 + 'token', + oauth_scope: 'https://outlook.office.com/IMAP.AccessAsUser.All offline_access openid email profile'}, + // Google issues a refresh token only for offline access, and again only after consent + google_user: {host: 'imap.gmail.com', auth_type: 'XOAUTH2', oauth_flow: 'authorization_code', + oauth_authorize_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', oauth_token_endpoint: 'https://oauth2.googleapis.com/token', + oauth_scope: 'https://mail.google.com/ openid email', oauth_extra_params: '{"access_type":"offline","prompt":"consent select_account"}'}, + gmail: {host: 'imap.gmail.com'}, + gmx: {host: 'imap.gmx.net'}, + webde: {host: 'imap.web.de'}, + tonline: {host: 'secureimap.t-online.de'}, + icloud: {host: 'imap.mail.me.com'}, + yahoo: {host: 'imap.mail.yahoo.com'}, + ionos: {host: 'imap.ionos.de'}, + strato: {host: 'imap.strato.de'} + }; + + // The name follows the preset until it is changed by hand; client ID and secret are kept + function applyPreset($link) { + var $form = $link.closest('form'); + var preset = $.extend({port: 993, enc: 'SSL', auth_type: 'PLAIN', oauth_flow: 'client_credentials', oauth_authorize_endpoint: '', + oauth_token_endpoint: '', oauth_userinfo_endpoint: '', oauth_scope: '', oauth_extra_params: ''}, presets[$link.data('preset')]); + var $name = $form.find('input[name="name"]'); + var names = $form.find('.imapsync-source-preset').map(function() { return $.trim($(this).text()); }).get(); + if (!$name.val() || names.indexOf($name.val()) !== -1) $name.val($.trim($link.text())); + // bootstrap-select 1.14 duplicates the button label on refresh after .val(), use its own setter + $.each(preset, function(field, value) { + var $f = $form.find('[name="' + field + '"]'); + if ($f.data('selectpicker')) { + $f.selectpicker('val', value); + } else { + $f.val(value); + } + }); + var needsTenant = preset.oauth_token_endpoint.indexOf('{tenant}') !== -1; + $form.find('.imapsync-source-tenant-row').toggle(needsTenant); + applyTenant($form); + sourceFormToggle($form); + if (needsTenant) $form.find('.imapsync-source-tenant').trigger('focus'); + } + + // M365: the tenant (ID or domain) is part of the endpoint URLs + function applyTenant($form) { + var tenant = encodeURIComponent($.trim($form.find('.imapsync-source-tenant').val() || '')) || '{tenant}'; + $form.find('input[name="oauth_authorize_endpoint"], input[name="oauth_token_endpoint"]').val(function(_, url) { + return url.replace(/^(https:\/\/login\.microsoftonline\.com\/)[^\/]*/, function(_, base) { return base + tenant; }); + }); + } + + // A visible endpoint must not keep the {tenant} placeholder; checked before the form is posted + function tenantValid($form) { + var missing = $form.find('input[name="oauth_authorize_endpoint"]:visible, input[name="oauth_token_endpoint"]:visible') + .filter(function() { return this.value.indexOf('{tenant}') !== -1; }).length > 0; + $form.find('.imapsync-source-tenant').toggleClass('inputMissingAttr', missing); + if (missing) { + $form.find('.imapsync-source-tenant-row').show(); + mailcow_alert_box(lang_danger.imapsync_source_tenant_missing, 'danger'); + } + return !missing; + } + + // ===== Wiring ===== + + function bind() { + injectStyle(); + + // custom params editor + $(document).on('change', '.imapsync-cp-opt', function() { + cpApplyValueState($(this).closest('.imapsync-cp-row')); + cpSerialize($(this).closest('.imapsync-cp-editor')); + }); + $(document).on('input', '.imapsync-cp-val', function() { + cpSerialize($(this).closest('.imapsync-cp-editor')); + }); + $(document).on('click', '.imapsync-cp-add', function() { + var $editor = $(this).closest('.imapsync-cp-editor'); + cpEnsureOptions(function() { cpAddRow($editor); }); + }); + $(document).on('click', '.imapsync-cp-remove', function() { + var $editor = $(this).closest('.imapsync-cp-editor'); + $(this).closest('.imapsync-cp-row').remove(); + cpSerialize($editor); + }); + + // job form + $(document).on('change', 'select.imapsync-source-select', function() { + applySourceSelection($(this)); + }); + // a token is bound to the local mailbox: changing it drops the connect result + $(document).on('change', 'form[data-id="add_syncjob"] select[name="username"]', function() { + var $form = $(this).closest('form'); + var $user1 = $form.find('input[name="user1"]'); + if ($user1.prop('readonly')) { + $user1.prop('readonly', false).val(''); + setConnectStatus($form, '', ''); + } + }); + $(document).on('click', '.imapsync-oauth-connect-btn', function() { + startConnect($(this).closest('form')); + }); + window.addEventListener('message', onOauthMessage); + $(document).on('click', '.imapsync-open-source-modal', function(e) { + e.preventDefault(); + var $current = $(this).closest('.modal'); + var open = function() { $('#addImapsyncSourceModal').modal('show'); }; + if ($current.length) { + $current.one('hidden.bs.modal', open).modal('hide'); + } else { + open(); + } + }); + // authorization_code: user1 comes from the provider sign-in, require it before posting + $('[data-action="add_item"][data-id="add_syncjob"], [data-action="edit_selected"][data-id="editsyncjob"]').on('click', function(e) { + var $form = $(this).closest('form'); + var $opt = $form.find('select.imapsync-source-select option:selected'); + if ($opt.attr('data-auth-type') === 'XOAUTH2' && $opt.attr('data-oauth-flow') === 'authorization_code' && + !$.trim($form.find('input[name="user1"]').val() || '')) { + e.preventDefault(); + e.stopImmediatePropagation(); + setConnectStatus($form, 'danger', lang_danger.imapsync_source_oauth_connect_required); + return false; + } + }); + + // source editor + $(document).on('change', 'select.imapsync-source-auth-type, select.imapsync-source-oauth-flow', function() { + sourceFormToggle($(this).closest('form')); + }); + $(document).on('change', 'select.imapsync-source-scope', function() { + sourceScopeToggle($(this).closest('form')); + }); + $('[data-action="add_item"][data-id="add_imapsync_source"], [data-action="edit_selected"][data-id="edit_imapsync_source"]').on('click', function(e) { + var $form = $(this).closest('form'); + if (!extraParamsValid($form) || !tenantValid($form)) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + $(document).on('click', '.imapsync-source-preset', function(e) { + e.preventDefault(); + applyPreset($(this)); + }); + $(document).on('input', '.imapsync-source-tenant', function() { + applyTenant($(this).closest('form')); + }); + $(document).on('click', '.imapsync-copy-btn', function(e) { + e.preventDefault(); + copyField($(this).data('copy-target')); + }); + + // modals + $(document).on('shown.bs.modal', '#addImapsyncSourceModal', function() { + var $form = $(this).find('form'); + sourceFormToggle($form); + sourceScopeToggle($form); + }); + $(document).on('shown.bs.modal', '#addSyncJobModalAdmin, #addSyncJobModal', function() { + populateSourceSelects(); + cpInit($(this)); + }); + + // edit page: forms are already rendered + $('form[data-id="edit_imapsync_source"]').each(function() { + sourceFormToggle($(this)); + sourceScopeToggle($(this)); + }); + $('form[data-id="editsyncjob"]').each(function() { + var $form = $(this); + $form.find('input[name="user1"]').data('initial', $form.find('input[name="user1"]').val()); + populateSourceSelects(); + cpInit($form); + }); + } + + $(document).ready(bind); + + return { + drawSourceTable: drawSourceTable, + jobTableStateSave: jobTableStateSave, + jobTableStateLoad: jobTableStateLoad, + prepareJobRow: prepareJobRow, + wrapCell: wrapCell, + L: L + }; +})(jQuery); diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 5ad5cceab..e073650d3 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -2169,7 +2169,11 @@ jQuery(function($){ "tr" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", language: lang_datatables, - order: [[2, 'desc']], + // keep the backend order (C4: prio desc, never run first, least recently run) + order: [], + // drop state saved before the rework (it would restore the old ID sort / column visibility) + stateSaveParams: mailcowImapsync.jobTableStateSave, + stateLoadParams: mailcowImapsync.jobTableStateLoad, initComplete: function(){ hideTableExpandCollapseBtn('#tab-syncjobs', '#sync_job_table'); }, @@ -2177,40 +2181,21 @@ jQuery(function($){ type: "GET", url: "/api/v1/get/syncjobs/all/no_log", dataSrc: function(json){ + if (!Array.isArray(json)) return []; $.each(json, function (i, item) { - item.log = '' + lang.open_logs + '' - item.user2 = escapeHtml(item.user2); if (!item.exclude > 0) { item.exclude = '-'; } else { item.exclude = '' + escapeHtml(item.exclude) + ''; } - item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.host1) + ':' + escapeHtml(item.port1); + mailcowImapsync.prepareJobRow(item); + item.user2 = mailcowImapsync.wrapCell(escapeHtml(item.user2)); item.action = ''; item.chkbox = ''; - if (item.is_running == 1) { - item.is_running = '' + lang.running + ''; - } else { - item.is_running = '' + lang.waiting + ''; - } - if (!item.last_run > 0) { - item.last_run = lang.waiting; - } - if (item.success == null) { - item.success = '-'; - item.exit_status = ''; - } else { - item.success = ''; - } - if (lang['syncjob_'+item.exit_status]) { - item.exit_status = lang['syncjob_'+item.exit_status]; - } else if (item.success != '-') { - item.exit_status = lang.syncjob_check_log; - } - item.exit_status = item.success + ' ' + item.exit_status; }); return json; @@ -2235,40 +2220,46 @@ jQuery(function($){ responsivePriority: 2 }, { - title: 'ID', + title: mailcowImapsync.L('id'), data: 'id', responsivePriority: 3, defaultContent: '' }, { - title: lang.owner, + title: lang.syncjobs.prio, + data: 'prio', + visible: role === 'admin', + responsivePriority: 3, + defaultContent: '' + }, + { + title: lang.syncjobs.user2_label, data: 'user2', responsivePriority: 4, defaultContent: '' }, { - title: 'Server', + title: lang.syncjobs.source_server, data: 'server_w_port', + responsivePriority: 6, defaultContent: '' }, { title: lang.last_run, data: 'last_run', + responsivePriority: 9, defaultContent: '' }, { - title: lang.syncjob_last_run_result, + title: lang.syncjobs.syncjob_last_run_result, data: 'exit_status', - defaultContent: '' - }, - { - title: 'Log', - data: 'log', + responsivePriority: 8, defaultContent: '' }, { title: lang.active, data: 'active', + responsivePriority: 10, defaultContent: '', render: function (data, type) { return 1==data?'1':0==data&&'0'; @@ -2277,6 +2268,7 @@ jQuery(function($){ { title: lang.status, data: 'is_running', + responsivePriority: 7, defaultContent: '' }, { @@ -2486,6 +2478,7 @@ jQuery(function($){ onVisible("[id^=alias_table]", () => draw_alias_table()); onVisible("[id^=aliasdomain_table]", () => draw_aliasdomain_table()); onVisible("[id^=sync_job_table]", () => draw_sync_job_table()); + onVisible("[id^=imapsync_source_table]", () => mailcowImapsync.drawSourceTable(role === 'admin' || acl_data.syncjobs === 1)); onVisible("[id^=filter_table]", () => draw_filter_table()); onVisible("[id^=bcc_table]", () => draw_bcc_table()); onVisible("[id^=recipient_map_table]", () => draw_recipient_map_table()); diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index 288e7abd2..af6521f9e 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -272,22 +272,27 @@ jQuery(function($){ "tr" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", language: lang_datatables, + // keep the backend order (C4: prio desc, never run first, least recently run) + order: [], + // drop state saved before the rework (it would restore the old ID sort / column visibility) + stateSaveParams: mailcowImapsync.jobTableStateSave, + stateLoadParams: mailcowImapsync.jobTableStateLoad, ajax: { type: "GET", url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log', dataSrc: function(data){ + if (!Array.isArray(data)) return []; $.each(data, function (i, item) { - item.user1 = escapeHtml(item.user1); - item.log = '' + lang.open_logs + '' if (!item.exclude > 0) { item.exclude = '-'; } else { item.exclude = '' + escapeHtml(item.exclude) + ''; } - item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1); + mailcowImapsync.prepareJobRow(item); if (acl_data.syncjobs === 1) { item.action = ''; item.chkbox = ''; @@ -296,26 +301,6 @@ jQuery(function($){ item.action = '-'; item.chkbox = ''; } - if (item.is_running == 1) { - item.is_running = '' + lang.running + ''; - } else { - item.is_running = '' + lang.waiting + ''; - } - if (!item.last_run > 0) { - item.last_run = lang.waiting; - } - if (item.success == null) { - item.success = '-'; - item.exit_status = ''; - } else { - item.success = ''; - } - if (lang['syncjob_'+item.exit_status]) { - item.exit_status = lang['syncjob_'+item.exit_status]; - } else if (item.success != '-') { - item.exit_status = lang.syncjob_check_log; - } - item.exit_status = item.success + ' ' + item.exit_status; }); return data; @@ -340,41 +325,34 @@ jQuery(function($){ responsivePriority: 2 }, { - title: 'ID', + title: mailcowImapsync.L('id'), data: 'id', defaultContent: '', responsivePriority: 3 }, { - title: 'Server', + title: lang.syncjobs.source_server, data: 'server_w_port', - defaultContent: '' - }, - { - title: lang.username, - data: 'user1', defaultContent: '', - responsivePriority: 3 + responsivePriority: 4 }, { title: lang.last_run, data: 'last_run', - defaultContent: '' + defaultContent: '', + responsivePriority: 7 }, { - title: lang.syncjob_last_run_result, + title: lang.syncjobs.syncjob_last_run_result, data: 'exit_status', - defaultContent: '' - }, - { - title: 'Log', - data: 'log', - defaultContent: '' + defaultContent: '', + responsivePriority: 6 }, { title: lang.active, data: 'active', defaultContent: '', + responsivePriority: 8, render: function (data, type) { return 1==data?'':0==data&&'' } @@ -385,11 +363,6 @@ jQuery(function($){ defaultContent: '', responsivePriority: 5 }, - { - title: lang.encryption, - data: 'enc1', - defaultContent: '' - }, { title: lang.excludes, data: 'exclude', @@ -699,6 +672,7 @@ jQuery(function($){ onVisible("[id^=bl_policy_mailbox_table]", () => draw_bl_policy_mailbox_table()); onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table()); onVisible("[id^=sync_job_table]", () => draw_sync_job_table()); + onVisible("[id^=imapsync_source_table]", () => mailcowImapsync.drawSourceTable(acl_data.syncjobs === 1)); onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table()); onVisible("[id^=recent-logins]", () => last_logins('get')); }); diff --git a/data/web/json_api.php b/data/web/json_api.php index fc34b2595..71c82f1d9 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -19,7 +19,7 @@ function api_log($_data) { if ($value) { if (is_array($value)) unset($value["csrf_token"]); foreach ($value as $key => &$val) { - if(preg_match("/pass/i", $key)) { + if(preg_match("/pass|secret|oauth_extra_params/i", $key)) { $val = '*'; } } @@ -317,7 +317,10 @@ if (isset($_GET['query'])) { process_add_return(admin('add', $attr)); break; case "syncjob": - process_add_return(mailbox('add', 'syncjob', $attr)); + process_add_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->addJob($attr)); + break; + case "syncjob_source": + process_add_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->addSource($attr)); break; case "bcc": process_add_return(bcc('add', $attr)); @@ -1046,61 +1049,37 @@ if (isset($_GET['query'])) { } process_get_return($data); break; + case "syncjob_options": + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getOptions()); + break; + case "imapsync_settings": + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSettings()); + break; + case "syncjob_source": + switch ($object) { + case "all": + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSources()); + break; + default: + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSource($object)); + break; + } + break; case "syncjobs": switch ($object) { case "all": - $domains = mailbox('get', 'domains'); - if (!empty($domains)) { - foreach ($domains as $domain) { - $mailboxes = mailbox('get', 'mailboxes', $domain); - if (!empty($mailboxes)) { - foreach ($mailboxes as $mailbox) { - $syncjobs = mailbox('get', 'syncjobs', $mailbox); - if (!empty($syncjobs)) { - foreach ($syncjobs as $syncjob) { - if (isset($extra)) { - $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra)); - } - else { - $details = mailbox('get', 'syncjob_details', $syncjob); - } - if ($details) { - $data[] = $details; - } - else { - continue; - } - } - } - } - } + // Admins see every job (incl. orphaned ones), others the jobs of the mailboxes they manage + $mailboxes = array(); + if ($_SESSION['mailcow_cc_role'] != 'admin') { + foreach (mailbox('get', 'domains') ?: array() as $domain) { + $mailboxes = array_merge($mailboxes, mailbox('get', 'mailboxes', $domain) ?: array()); } - process_get_return($data); - } - else { - echo '{}'; } + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getAllJobs($extra, $mailboxes)); break; default: - $syncjobs = mailbox('get', 'syncjobs', $object); - if (!empty($syncjobs)) { - foreach ($syncjobs as $syncjob) { - if (isset($extra)) { - $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra)); - } - else { - $details = mailbox('get', 'syncjob_details', $syncjob); - } - if ($details) { - $data[] = $details; - } - else { - continue; - } - } - } - process_get_return($data); + process_get_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJobs($object, $extra)); break; } break; @@ -1742,7 +1721,10 @@ if (isset($_GET['query'])) { process_delete_return(rsettings('delete', array('id' => $items))); break; case "syncjob": - process_delete_return(mailbox('delete', 'syncjob', array('id' => $items))); + process_delete_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->deleteJobs($items)); + break; + case "syncjob_source": + process_delete_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->deleteSources($items)); break; case "filter": process_delete_return(mailbox('delete', 'filter', array('id' => $items))); @@ -1968,7 +1950,27 @@ if (isset($_GET['query'])) { } break; case "syncjob": - process_edit_return(mailbox('edit', 'syncjob', array_merge(array('id' => $items), $attr))); + switch ($object) { + case "run_next": + process_edit_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->runNext($items)); + break; + default: + process_edit_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->editJobs($items, $attr)); + break; + } + break; + case "imapsync_settings": + process_edit_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->editSettings($attr)); + break; + case "syncjob_source": + switch ($object) { + case "refresh_token": + process_edit_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->refreshSourceToken($items)); + break; + default: + process_edit_return(\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->editSources($items, $attr)); + break; + } break; case "filter": process_edit_return(mailbox('edit', 'filter', array_merge(array('id' => $items), $attr))); diff --git a/data/web/lang/lang.bg-bg.json b/data/web/lang/lang.bg-bg.json index d17c4dd2b..b67bcc93f 100644 --- a/data/web/lang/lang.bg-bg.json +++ b/data/web/lang/lang.bg-bg.json @@ -44,24 +44,16 @@ "app_name": "Име на приложението", "app_password": "Добавяне на парола за приложение", "app_passwd_protocols": "Разрешени протоколи за паролата на приложението", - "automap": "Опит за автоматично картографиране на папки (\"Изпратени\", \"Изпратени\" => \"Изпратени\" и т.н.)", "backup_mx_options": "Опции за реле", "bcc_dest_format": "BCC дестинацията трябва да бъде един валиден имейл адрес.
Ако имате нужда да изпратите копие до множество адреси, създайте псевдоним и го използвайте тук.", "comment_info": "Частен коментар не е видим за потребителя, докато публичен коментар се показва като подсказка при преминаване с мишката върху него в прегледа на потребителя", - "custom_params": "Персонализирани параметри", - "custom_params_hint": "Дясно: --param=xy, грешно: --param xy", - "delete1": "Изтриване от източника след завършване", - "delete2": "Изтриване на съобщенията в дестинацията, които не са в източника", - "delete2duplicates": "Изтриване на дубликати в дестинацията", "description": "Описание", "destination": "Дестинация", "disable_login": "Забраняване на вход (входящите съобщения все още се приемат)", "domain": "Домейн", "domain_matches_hostname": "Домейнът %s съвпада с името на хоста", "domain_quota_m": "Обща квота на домейна (MiB)", - "dry": "Симулиране на синхронизация", "enc_method": "Метод на криптиране", - "exclude": "Изключване на обекти (regex)", "full_name": "Пълно име", "gal": "Глобален адресен списък", "gal_info": "Глобалният адресен списък съдържа всички обекти на домейна и не може да бъде редактиран от нито един потребител. Липсва информация за заетост/свободно време в SOGo, ако е деактивирано! Рестартирайте SOGo, за да приложите промените.", @@ -77,7 +69,6 @@ "mailbox_username": "Потребителско име (лява част на имейл адрес)", "max_aliases": "Макс. възможни псевдоними", "max_mailboxes": "Макс. възможни пощенски кутии", - "mins_interval": "Интервал за проверка (минути)", "multiple_bookings": "Множествени резервации", "nexthop": "Следващ хоп", "password": "Парола", @@ -97,16 +88,10 @@ "select_domain": "Моля, изберете първо домейн", "sieve_desc": "Кратко описание", "sieve_type": "Тип на филтър", - "skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)", - "subscribeall": "Абониране за всички папки", - "syncjob": "Добавяне на синхронизираща задача", - "syncjob_hint": "Имайте предвид, че паролите трябва да бъдат запазени като обикновен текст!", "tags": "Тагове", "target_address": "Адреси за пренасочване", "target_address_info": "Пълни имейл адреси (разделени с запетая).", "target_domain": "Целеви домейн", - "timeout1": "Таймаут за връзка с отдалечен хост", - "timeout2": "Таймаут за връзка с локален хост", "username": "Потребителско име", "validate": "Валидиране", "validation_success": "Успешно валидиране" @@ -630,7 +615,6 @@ "app_name": "Име на приложението", "app_passwd": "Парола за приложение", "app_passwd_protocols": "Разрешени протоколи за паролата на приложението", - "automap": "Опит за автоматично картографиране на папки (\"Изпратени\", \"Изпратени\" => \"Изпратени\" и т.н.)", "backup_mx_options": "Опции за реле", "bcc_dest_format": "BCC дестинацията трябва да бъде един валиден имейл адрес.
Ако имате нужда да изпратите копие до множество адреси, създайте псевдоним и го използвайте тук.", "client_id": "Идентификатор на клиента", @@ -638,9 +622,6 @@ "comment_info": "Частен коментар не е видим за потребителя, докато публичен коментар се показва като подсказка при преминаване с мишката върху него в прегледа на потребителя", "created_on": "Създаден на", "custom_attributes": "Персонализирани атрибути", - "delete1": "Изтриване от източника след завършване", - "delete2": "Изтриване на съобщенията в дестинацията, които не са в източника", - "delete2duplicates": "Изтриване на дубликати в дестинацията", "delete_ays": "Моля, потвърдете процеса на изтриване.", "description": "Описание", "disable_login": "Забраняване на вход (входящите съобщения все още се приемат)", @@ -664,7 +645,6 @@ "dont_check_sender_acl": "Деактивиране на проверката на изпращач за домейн %s (+ домейни на псевдоними)", "edit_alias_domain": "Редактиране на домейн на псевдоним", "encryption": "Криптиране", - "exclude": "Изключване на обекти (regex)", "extended_sender_acl": "Външни адреси на изпращач", "extended_sender_acl_info": "Трябва да бъде импортиран DKIM домейн ключ, ако е наличен.
\r\n Помнете да добавите този сървър към съответния SPF TXT запис.
\r\n Когато домейн или домейн на псевдоним бъде добавен към този сървър, който се припокрива с външен адрес, външният адрес ще бъде премахнат.
\r\n Използвайте @domain.tld, за да разрешите изпращане като *@domain.tld.", "footer_exclude": "Изключване от подвал", @@ -691,10 +671,7 @@ "max_aliases": "Макс. псевдоними", "max_mailboxes": "Макс. възможни пощенски кутии", "max_quota": "Макс. квота за пощенска кутия (MiB)", - "maxage": "Максимална възраст на съобщенията в дни, които ще бъдат проверени от отдалечен
(0 = игнорирай възраст)", - "maxbytespersecond": "Макс. байтове за секунда
(0 = неограничен)", "mbox_rl_info": "Това ограничение на скоростта се прилага върху SASL името за вход, то съвпада с всеки \"от\" адрес, използван от влезлия потребител. Ограничението на скоростта на пощенска кутия пренарежда ограничението на скоростта за домейн.", - "mins_interval": "Интервал (мин)", "multiple_bookings": "Множествени резервации", "nexthop": "Следващ хоп", "none_inherit": "Без / Наследяване", @@ -735,7 +712,6 @@ "sender_acl_info": "Ако потребителят на пощенска кутия A е разрешен да изпраща като потребителя на пощенска кутия B, адресът на изпращач не се показва автоматично като избираем \"от\" поле в SOGo.
\r\n Потребителят на пощенска кутия B трябва да създаде делегация в SOGo, за да разреши на потребителя на пощенска кутия A да избира техния адрес като изпращач. За да делегирате пощенска кутия в SOGo, използвайте менюто (три точки) вдясно от името на пощенската кутия в горния ляв ъгъл, докато сте в режим на поща. Това поведение не се прилага за адреси на псевдоними.", "sieve_desc": "Кратко описание", "sieve_type": "Тип на филтър", - "skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)", "sogo_access": "Директно препращане към SOGo", "sogo_access_info": "След влизане, потребителят се пренасочва автоматично към SOGo.", "sogo_visible": "Псевдонимът е видим в SOGo", @@ -744,12 +720,8 @@ "spam_filter": "Филтър за спам", "spam_policy": "Добавяне или премахване на елементи към черния/бял списък", "spam_score": "Задаване на персонализиран резултат за спам", - "subfolder2": "Синхронизиране в подпапка в дестинацията
(празно = не използвай подпапка)", - "syncjob": "Редактиране на синхронизираща задача", "target_address": "Адрес/и за пренасочване (разделени с запетая)", "target_domain": "Целеви домейн", - "timeout1": "Таймаут за връзка с отдалечен хост", - "timeout2": "Таймаут за връзка с локален хост", "title": "Редактиране на обект", "unchanged_if_empty": "Ако не е променено, оставете празно", "username": "Потребителско име", @@ -964,16 +936,6 @@ "spam_aliases": "Времеви псевдоними", "stats": "Статистика", "status": "Статус", - "sync_jobs": "Синхронизиращи задачи", - "syncjob_check_log": "Проверка на лога", - "syncjob_last_run_result": "Резултат от последното изпълнение", - "syncjob_EX_OK": "Успех", - "syncjob_EXIT_CONNECTION_FAILURE": "Проблем с връзката", - "syncjob_EXIT_TLS_FAILURE": "Проблем с криптираната връзка", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблем с удостоверяването", - "syncjob_EXIT_OVERQUOTA": "Целевата пощенска кутия е над квотата", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не може да се свърже с отдалечен сървър", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Грешно потребителско име или парола", "table_size": "Размер на таблицата", "table_size_show_n": "Показване на %s елемента", "target_address": "Адрес за пренасочване", @@ -1204,6 +1166,37 @@ "waiting_usb_register": "Изчакване на USB устройство...

Моля, въведете паролата си по-горе и потвърдете регистрацията си, като докоснете бутона на вашето USB устройство.", "yubi_otp": "Удостоверяване с Yubico OTP" }, + "syncjobs": { + "sync_jobs": "Синхронизиращи задачи", + "syncjob_check_log": "Проверка на лога", + "syncjob_last_run_result": "Резултат от последното изпълнение", + "syncjob_EX_OK": "Успех", + "syncjob_EXIT_CONNECTION_FAILURE": "Проблем с връзката", + "syncjob_EXIT_TLS_FAILURE": "Проблем с криптираната връзка", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблем с удостоверяването", + "syncjob_EXIT_OVERQUOTA": "Целевата пощенска кутия е над квотата", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не може да се свърже с отдалечен сървър", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Грешно потребителско име или парола", + "add": "Добавяне на синхронизираща задача", + "add_hint": "Имайте предвид, че паролите трябва да бъдат запазени като обикновен текст!", + "edit": "Редактиране на синхронизираща задача", + "create": "Създаване на нова синхронизираща задача", + "dry": "Симулиране на синхронизация", + "subfolder2": "Синхронизиране в подпапка в дестинацията
(празно = не използвай подпапка)", + "mins_interval": "Интервал за проверка (минути)", + "maxage": "Максимална възраст на съобщенията в дни, които ще бъдат проверени от отдалечен
(0 = игнорирай възраст)", + "exclude": "Изключване на обекти (regex)", + "automap": "Опит за автоматично картографиране на папки (\"Изпратени\", \"Изпратени\" => \"Изпратени\" и т.н.)", + "delete1": "Изтриване от източника след завършване", + "delete2": "Изтриване на съобщенията в дестинацията, които не са в източника", + "delete2duplicates": "Изтриване на дубликати в дестинацията", + "skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)", + "subscribeall": "Абониране за всички папки", + "timeout1": "Таймаут за връзка с отдалечен хост", + "timeout2": "Таймаут за връзка с локален хост", + "maxbytespersecond": "Макс. байтове за секунда
(0 = неограничен)", + "custom_params": "Персонализирани параметри" + }, "user": { "action": "Действие", "active": "Активен", @@ -1234,7 +1227,6 @@ "clear_recent_successful_connections": "Изчистване на видимите успешни връзки", "client_configuration": "Показване на ръководства за настройка на пощенски клиенти и смартфони", "create_app_passwd": "Създаване на парола за приложение", - "create_syncjob": "Създаване на нова синхронизираща задача", "created_on": "Създаден на", "daily": "Ежедневно", "day": "ден", @@ -1340,16 +1332,6 @@ "spamfilter_wl_desc": "Адресите в белия списък никога ще бъдат класифицирани като спам. Поддържат се уайлдкардове.
Съобщенията, които не са класифицирани като спам, ще бъдат доставени в папката за нежелана поща, ако резултатът за спам е над стойността за висок резултат за спам.", "spamfilter_yellow": "Жълто: това съобщение може да е спам, ще бъде маркирано като спам и преместено в папката за нежелана поща", "status": "Статус", - "sync_jobs": "Синхронизиращи задачи", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблем с удостоверяването", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Грешно потребителско име или парола", - "syncjob_EXIT_CONNECTION_FAILURE": "Проблем с връзката", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не може да се свърже с отдалечен сървър", - "syncjob_EXIT_OVERQUOTA": "Целевата пощенска кутия е над квотата", - "syncjob_EXIT_TLS_FAILURE": "Проблем с криптираната връзка", - "syncjob_EX_OK": "Успех", - "syncjob_check_log": "Проверка на лога", - "syncjob_last_run_result": "Резултат от последното изпълнение", "tag_handling": "Настройка за тагове", "tag_help_example": "Пример за тагован имейл адрес: me+Facebook@example.org", "tag_help_explain": "В подпапка: ще бъде създадена нова подпапка под INBOX (\"INBOX/Facebook\").
\r\nВ тема: първото име на тага ще бъде добавено към темата на съобщението, например: \"[Facebook] Моите новини\".", diff --git a/data/web/lang/lang.ca-es.json b/data/web/lang/lang.ca-es.json index 5b8e9b64d..6f23983d2 100644 --- a/data/web/lang/lang.ca-es.json +++ b/data/web/lang/lang.ca-es.json @@ -40,16 +40,11 @@ "alias_address_info": "Adreces de correu completes, o @exemple.com per a atrapar tots els missatges per a un domini (separats per coma). Només dominis interns.", "alias_domain": "Domini àlies", "alias_domain_info": "Només noms de domini vàlids (separats per coma).", - "automap": "Provar de mapejar automàticament (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Opcions backup MX:", - "delete1": "Esborrar de l'origen un cop s'han copiat", - "delete2": "Esborrar els missatges a destí que no son al origen", - "delete2duplicates": "Eliminar els duplicats al destí", "description": "Descripció:", "domain": "Domini", "domain_quota_m": "Quota total del domini (MiB):", "enc_method": "Mètode de xifrat", - "exclude": "Excloure els objectes (regex)", "full_name": "Nom complet:", "goto_null": "Descartar mail silenciosament", "hostname": "Hostname", @@ -58,7 +53,6 @@ "mailbox_username": "Nom d'usuari (part de l'esquerra de l'adreça @):", "max_aliases": "Màx. àlies possibles:", "max_mailboxes": "Màx. bústies possibles:", - "mins_interval": "Intèrval (minuts)", "multiple_bookings": "Reserves múltiples", "password": "Constrasenya:", "password_repeat": "Confirmació de contrasenya (repetir):", @@ -72,9 +66,6 @@ "select_domain": "Si et plau, primer selecciona un domini", "sieve_desc": "Short description", "sieve_type": "Filter type", - "skipcrossduplicates": "Saltar missatges duplicats entre carpetes (només es desa el primer)", - "syncjob": "Afegir sync job", - "syncjob_hint": "Tingues en compte que les contrasenyes es desen sense xifrar!", "target_address": "Adreces goto:", "target_address_info": "Adreces de corru completes (separades per coma).", "target_domain": "Domini destí:", @@ -86,12 +77,9 @@ "app_passwd_protocols": "Protocols autoritzats per la contrasenya de l'aplicació", "bcc_dest_format": "La destinació c/o ha de ser una única adreça de correu vàlida.
Si necessiteu enviar una còpia a diverses adreces, creeu un àlies i utilitzeu-lo aquí.", "comment_info": "Els comentaris privats no són visibles per l'usuari, mentre que els comentaris públics apareixen com una descripció emergent a la informació de l'usuari", - "custom_params": "Paràmetres personalitzats", - "custom_params_hint": "Correcte: --param=xy, incorrecte: --param xy", "destination": "Destí", "disable_login": "No permetre l'inici de sessió (els missatges entrants continuen sent acceptats)", "domain_matches_hostname": "El domini %s coincideix amb el nom del servidor", - "dry": "Simular la sincronització", "gal": "Llista d'adreces global", "generate": "genereu", "inactive": "Inactiu", @@ -249,11 +237,7 @@ "edit": { "active": "Actiu", "alias": "Editar àlies", - "automap": "Provar de mapejar automàticament (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Opcions backup MX:", - "delete1": "Esborrar de l'origen un cop s'han copiat", - "delete2": "Esborrar els missatges a destí que no son al origen", - "delete2duplicates": "Eliminar els duplicats al destí", "description": "Descripció:", "domain": "Editar domini", "domain_admin": "Editar administrador del domini", @@ -262,7 +246,6 @@ "dont_check_sender_acl": "No verifiques remitente para el dominio %s", "edit_alias_domain": "Editar àlies de domini", "encryption": "Xifrat", - "exclude": "Excloure els objectes (regex)", "force_pw_update": "Forçar l'actualització de la contrassenya al proper login", "force_pw_update_info": "Aquest usuari només podrà accedir a la interfície de gestió.", "full_name": "Nom complet", @@ -272,9 +255,6 @@ "max_aliases": "Màx. àlies:", "max_mailboxes": "Màx. bústies possibles:", "max_quota": "Màx. quota per bústia (MiB):", - "maxage": "Salta missatges més vells del número de dies
(0 = no salta res)", - "maxbytespersecond": "Màx. bytes per segon
(0 = il.limitat)", - "mins_interval": "Intèrval (min)", "multiple_bookings": "Reserves múltiples", "password": "Contrasenya:", "password_repeat": "Confirmació de contrasenya (repetir):", @@ -289,12 +269,9 @@ "sender_acl": "Permetre enviar com a", "sieve_desc": "Short description", "sieve_type": "Filter type", - "skipcrossduplicates": "Saltar missatges duplicats entre carpetes (només es desa el primer)", "spam_alias": "Crear o canviar alies temporals limitats per temps", "spam_policy": "Afegeix o treu elementes a la white-/blacklist", "spam_score": "Especifica una puntuació de spam", - "subfolder2": "Sincronitza a la subcarpeta destí
(buit = no usar subcarpeta)", - "syncjob": "Sync job", "target_address": "Direcció/ns goto (separades per coma):", "target_domain": "Domini destí:", "title": "Editar l'objecte", @@ -405,7 +382,6 @@ "sieve_info": "Podeu emmagatzemar diversos filtres per usuari, però només un filtre previ i un filtre posterior poden estar actius al mateix temps.
\r\nCada filtre es processarà en l'ordre descrit. Ni un script que falli, ni un \"keep\" emès farà que es deixin de processar els altres scripts.
\r\nFiltre previ → Filtre d'usuari → Filtre posterior → Filtre global", "spam_aliases": "Temp. àlies", "status": "Estat", - "sync_jobs": "Sync jobs", "target_address": "Direcció Goto", "target_domain": "Domini destí", "tls_enforce_in": "Forçar TLS al rebre", @@ -500,6 +476,25 @@ "waiting_usb_register": "Esperant el dispositiu USB...

Posa el teu password i confirma el registre del teu WebAuthn apretant el botó del teu dispositiiu USB WebAuthn.", "yubi_otp": "Autenticació OTP de Yubico" }, + "syncjobs": { + "sync_jobs": "Feines de sincronització", + "add": "Afegir sync job", + "add_hint": "Tingues en compte que les contrasenyes es desen sense xifrar!", + "edit": "Sync job", + "create": "Afegir treball de sincronitzaió", + "dry": "Simular la sincronització", + "subfolder2": "Sincronitza a la subcarpeta destí
(buit = no usar subcarpeta)", + "mins_interval": "Intèrval (minuts)", + "maxage": "Salta missatges més vells del número de dies
(0 = no salta res)", + "exclude": "Excloure els objectes (regex)", + "automap": "Provar de mapejar automàticament (\"Sent items\", \"Sent\" => \"Sent\" etc.)", + "delete1": "Esborrar de l'origen un cop s'han copiat", + "delete2": "Esborrar els missatges a destí que no son al origen", + "delete2duplicates": "Eliminar els duplicats al destí", + "skipcrossduplicates": "Saltar missatges duplicats entre carpetes (només es desa el primer)", + "maxbytespersecond": "Màx. bytes per segon
(0 = il.limitat)", + "custom_params": "Paràmetres personalitzats" + }, "user": { "action": "Acció", "active": "Actiu", @@ -516,7 +511,6 @@ "aliases_send_as_all": "Al enviar no es verificarà l'adreça remitent per aquests dominis:", "change_password": "Canviar la contrasenya", "client_configuration": "Guies de configuració per als clients de correu més habituals", - "create_syncjob": "Afegir treball de sincronitzaió", "day": "Dia", "description": "Descripció", "direct_aliases": "Adreces àlies directes", @@ -567,7 +561,6 @@ "spamfilter_wl_desc": "Les adreces de la lista blanca mai es clasificaran como a spam. Es pot fer serivir *@exemple.org.", "spamfilter_yellow": "Groc: el missatge pot ser spam, s'etiquetarà com a spam i es mourà a la carpeta de correu brossa", "status": "Estat", - "sync_jobs": "Feines de sincronització", "tag_handling": "Al rebre un missatge etiquetat", "tag_help_example": "Ejemplo de una dirección email etiquetada: mi+Facebook@ejemplo.org", "tag_help_explain": "Moure a subcarpeta: es crearà una subcarpeta anomenada com la etiqueta a INBOX (\"INBOX/Facebook\") i es mourà el correu allà.
\r\nMarcar al assumpte: s'afegirà el nom de la etiqueta al assumpte del missatge, per exemple: \"[Facebook] Les meves notícies\".", diff --git a/data/web/lang/lang.cs-cz.json b/data/web/lang/lang.cs-cz.json index 708de8486..3964c8ca0 100644 --- a/data/web/lang/lang.cs-cz.json +++ b/data/web/lang/lang.cs-cz.json @@ -44,15 +44,9 @@ "app_name": "Název aplikace", "app_passwd_protocols": "Povolené protokoly pro hesla aplikací", "app_password": "Přidat heslo aplikace", - "automap": "Pokusit se automaticky mapovat složky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", "backup_mx_options": "Možnosti záložního MX", "bcc_dest_format": "Cíl kopie musí být jedna platná email adresa. Pokud potřebujete posílat kopie na více adres, vytvořte Alias a použijte jej zde.", "comment_info": "Soukromý komentář se nezobrazí uživateli; veřejný komentář se zobrazí jako nápověda při zastavení se kurzorem v přehledu uživatelů", - "custom_params": "Vlastní parametry", - "custom_params_hint": "Správně: --param=xy, špatně: --param xy", - "delete1": "Odstranit ze zdrojové schránky, po dokončení přenosu", - "delete2": "Odstranit zprávy v cílové schránce, pokud nejsou ve zdrojové", - "delete2duplicates": "Odstranit duplicity v cílové schránce", "description": "Popis", "destination": "Cíl", "disable_login": "Zakázat přihlášení (ale stále přijímat poštu)", @@ -60,7 +54,6 @@ "domain_matches_hostname": "Doména %s se shoduje s názvem hostitele", "domain_quota_m": "Celková kvóta domény (MiB)", "enc_method": "Způsob šifrování", - "exclude": "Vyloučené objekty (regex)", "full_name": "Celé jméno", "gal": "Globální seznam adres", "gal_info": "Globální seznam adres obsahuje všechny objekty v doméně a uživatel jej nemůže měnit. Pokud je vypnut, budou v SOGo chybět informace o obsazenosti! Při změně nutno restartovat SOGo.", @@ -76,7 +69,6 @@ "mailbox_username": "Uživatelské jméno (levá část email adresy)", "max_aliases": "Max. počet aliasů", "max_mailboxes": "Max. počet mailových schránek", - "mins_interval": "Interval kontroly (minuty)", "multiple_bookings": "Vícenásobné rezervace", "nexthop": "Další skok", "password": "Heslo", @@ -96,20 +88,13 @@ "select_domain": "Nejdříve vyberte doménu", "sieve_desc": "Krátký popis", "sieve_type": "Typ filtru", - "skipcrossduplicates": "Přeskočit duplicitní zprávy (\"první přijde, první mele\")", - "subscribeall": "Synchronizovat všechny složky", - "syncjob": "Přidat synchronizační úlohu", - "syncjob_hint": "Upozornění: Heslo bude uloženo jako prostý text!", "target_address": "Cílové adresy", "target_address_info": "Kompletní email adresa/y (oddělené čárkami).", "target_domain": "Cílová doména", - "timeout1": "Časový limit pro připojení ke vzdálenému serveru", - "timeout2": "Časový limit pro připojení k lokálnímu serveru", "username": "Uživatelské jméno", "validate": "Ověřit", "validation_success": "Úspěšně ověřeno", "tags": "Štítky", - "dry": "Simulovat synchronizaci", "internal": "Interní", "internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů.", "sender_allowed": "Povolit jako alias pro odesílání" @@ -645,16 +630,12 @@ "app_name": "Název aplikace", "app_passwd": "Heslo aplikace", "app_passwd_protocols": "Povolené protokoly pro hesla aplikací", - "automap": "Pokusit se automaticky mapovat složky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", "backup_mx_options": "Možnosti záložního MX", "bcc_dest_format": "Cíl kopie musí být jedna platná email adresa. Pokud potřebujete posílat kopie na více adres, vytvořte Alias a použijte jej zde.", "client_id": "ID klienta", "client_secret": "Tajný klíč klienta", "created_on": "Vytvoreno", "comment_info": "Soukromý komentář se nezobrazí uživateli; veřejný komentář se zobrazí jako nápověda při zastavení se kurzorem v přehledu uživatelů", - "delete1": "Odstranit ze zdrojové schránky, po dokončení přenosu", - "delete2": "Odstranit zprávy v cílové schránce, pokud nejsou ve zdrojové", - "delete2duplicates": "Odstranit duplicity v cílové schránce", "delete_ays": "Potvrďte odstranění.", "description": "Popis", "disable_login": "Zakázat přihlášení (ale stále přijímat poštu)", @@ -665,7 +646,6 @@ "dont_check_sender_acl": "Vypnout kontrolu odesílatele pro doménu %s (+ doménové aliasy)", "edit_alias_domain": "Upravit doménový alias", "encryption": "Šifrování", - "exclude": "Vyloučit objekty (regex)", "extended_sender_acl": "Externí adresy odesílatele", "extended_sender_acl_info": "Je dobré importovat DKIM klíč domény, pokud existuje.
\r\n Nezapomeňte přidat tento server do příslušného záznamu SPF TXT.
\r\n Je-li na tomto serveru vytvořena doména nebo doménový alias, který se shoduje s externí adresou, je tato externí adresa smazána.
\r\n Použije formát @domain.tld, chcete-li odesílat jako *@domain.tld.", "force_pw_update": "Vynutit změnu hesla při příštím přihlášení", @@ -686,10 +666,7 @@ "max_aliases": "Max. počet aliasů", "max_mailboxes": "Max. počet mailových schránek", "max_quota": "Max. kvóta mailové schránky (MiB)", - "maxage": "Maximální stáří stahovaných zpráv, ve dnech
(0 = ignorovat stáří)", - "maxbytespersecond": "Max. bajtů za sekundu
(0 = neomezeno)", "mbox_rl_info": "Toto omezení provozu se vyhodnocuje podle přihlašovacího jména SASL, porovnává se s jakoukoliv adresou \"od\" použitou přihlášeným uživatelem. Omezení provozu mailové schránky má prioritu před omezením provozu domény.", - "mins_interval": "Interval (min)", "multiple_bookings": "Vícenásobné rezervace", "nexthop": "Další skok", "none_inherit": "Žádné / Podědit", @@ -727,7 +704,6 @@ "sender_acl_info": "Má-li uživatel schránky A dovoleno odesílat jako uživatel schránky B, nezobrazuje se adresa odesílatele B v seznamu \"Od\" v SOGo automaticky.
\r\n Uživatel schránky A musí nejdříve v SOGo vytvořit pověření, jež umožní uživateli B vybrat svou adresu jako odesílatele. Tento mechanismus neplatí pro aliasy.", "sieve_desc": "Krátký popis", "sieve_type": "Typ filtru", - "skipcrossduplicates": "Přeskočit duplicitní zprávy (\"první přijde, první mele\")", "sogo_access": "Přímé předání na SOGo", "sogo_access_info": "Po přihlášení je uživatel automaticky přesměrován do služby SOGo.", "sogo_visible": "Alias dostupný v SOGo", @@ -736,12 +712,8 @@ "spam_filter": "Spam filtr", "spam_policy": "Přidat nebo odebrat položky seznamu", "spam_score": "Nastavte vlastní skóre spamu", - "subfolder2": "Synchronizace do podsložky v cílovém umístění
(prázdné = nepoužívat podsložku)", - "syncjob": "Upravit synchronizační úlohu", "target_address": "Cílová adresa/y (oddělte čárkou)", "target_domain": "Cílová doména", - "timeout1": "Časový limit pro připojení ke vzdálenému serveru", - "timeout2": "Časový limit pro připojení k lokálnímu serveru", "title": "Úprava objektu", "unchanged_if_empty": "Pokud se nemění, ponechte prázdné", "username": "Uživatelské jméno", @@ -997,16 +969,6 @@ "spam_aliases": "Dočasný alias", "stats": "Statistika", "status": "Stav", - "sync_jobs": "Synchronizační úlohy", - "syncjob_check_log": "Zkontrolujte záznam", - "syncjob_last_run_result": "Výsledek posledního spuštění", - "syncjob_EX_OK": "Úspěch", - "syncjob_EXIT_CONNECTION_FAILURE": "Problém se spojením", - "syncjob_EXIT_TLS_FAILURE": "Problém se šifrovaným spojením", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikací", - "syncjob_EXIT_OVERQUOTA": "Cílová schránka je plná", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nelze se připojit ke vzdálenému serveru", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživatelské jméno nebo heslo", "table_size": "Velikost tabulky", "table_size_show_n": "Zobrazit %s položek", "target_address": "Cílová adresa", @@ -1241,6 +1203,37 @@ "authenticators": "Autentifikátory", "u2f_deprecated_important": "Registrujte svůj klíč novou metodou WebAuthn ve správě správců." }, + "syncjobs": { + "sync_jobs": "Synchronizační úlohy", + "syncjob_check_log": "Zkontrolujte záznam", + "syncjob_last_run_result": "Výsledek posledního spuštění", + "syncjob_EX_OK": "Úspěch", + "syncjob_EXIT_CONNECTION_FAILURE": "Problém se spojením", + "syncjob_EXIT_TLS_FAILURE": "Problém se šifrovaným spojením", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikací", + "syncjob_EXIT_OVERQUOTA": "Cílová schránka je plná", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nelze se připojit ke vzdálenému serveru", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživatelské jméno nebo heslo", + "add": "Přidat synchronizační úlohu", + "add_hint": "Upozornění: Heslo bude uloženo jako prostý text!", + "edit": "Upravit synchronizační úlohu", + "create": "Vytvořit novou synchronizační úlohu", + "dry": "Simulovat synchronizaci", + "subfolder2": "Synchronizace do podsložky v cílovém umístění
(prázdné = nepoužívat podsložku)", + "mins_interval": "Interval kontroly (minuty)", + "maxage": "Maximální stáří stahovaných zpráv, ve dnech
(0 = ignorovat stáří)", + "exclude": "Vyloučené objekty (regex)", + "automap": "Pokusit se automaticky mapovat složky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", + "delete1": "Odstranit ze zdrojové schránky, po dokončení přenosu", + "delete2": "Odstranit zprávy v cílové schránce, pokud nejsou ve zdrojové", + "delete2duplicates": "Odstranit duplicity v cílové schránce", + "skipcrossduplicates": "Přeskočit duplicitní zprávy (\"první přijde, první mele\")", + "subscribeall": "Synchronizovat všechny složky", + "timeout1": "Časový limit pro připojení ke vzdálenému serveru", + "timeout2": "Časový limit pro připojení k lokálnímu serveru", + "maxbytespersecond": "Max. bajtů za sekundu
(0 = neomezeno)", + "custom_params": "Vlastní parametry" + }, "user": { "action": "Akce", "active": "Aktivní", @@ -1269,7 +1262,6 @@ "clear_recent_successful_connections": "Vymazat nedávné úspěšné přihlášení", "client_configuration": "Zobrazit průvodce nastavením e-mailových klientů a smartphonů", "create_app_passwd": "Vytvořit heslo aplikace", - "create_syncjob": "Vytvořit novou synchronizační úlohu", "created_on": "Vytvoreno", "daily": "Každý den", "day": "den", @@ -1370,16 +1362,6 @@ "spamfilter_wl_desc": "Povolené emailové adresy nebudou nikdy klasifikovány jako spam. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou mailovou schránkou), s výjimkou doménových košů a samotné mailové schránky.", "spamfilter_yellow": "Žlutá: tato zpráva může být spam, bude označena jako spam a přesunuta do složky nevyžádané pošty", "status": "Stav", - "sync_jobs": "Synchronizační úlohy", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikací", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživatelské jméno nebo heslo", - "syncjob_EXIT_CONNECTION_FAILURE": "Problém se spojením", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nelze se připojit ke vzdálenému serveru", - "syncjob_EXIT_OVERQUOTA": "Cílová schránka je plná", - "syncjob_EXIT_TLS_FAILURE": "Problém se šifrovaným spojením", - "syncjob_EX_OK": "Úspěch", - "syncjob_check_log": "Zkontrolujte záznam", - "syncjob_last_run_result": "Výsledek posledního spuštění", "tag_handling": "Zacházení s označkovanou poštou", "tag_help_example": "Příklad e-mailové adresy se značkou: me+Facebook@example.org", "tag_help_explain": "V podsložce: v doručené poště bude vytvořena nová podsložka pojmenovaná po značce zprávy (\"INBOX / Facebook\").
\r\nV předmětu: název značky bude přidáván k předmětu mailu, například: \"[Facebook] Moje zprávy\".", diff --git a/data/web/lang/lang.da-dk.json b/data/web/lang/lang.da-dk.json index f71fcfec2..26707aaa2 100644 --- a/data/web/lang/lang.da-dk.json +++ b/data/web/lang/lang.da-dk.json @@ -42,14 +42,8 @@ "alias_domain_info": "Kun gyldige domænenavne (kommasepareret).", "app_name": "App navn", "app_password": "Tilføj app-adgangskode", - "automap": "Prøv at automatisere mapper (\"Sendte emner\", \"Sendt\" => \"Sendt\" etc.)", "backup_mx_options": "Relæindstillinger", "comment_info": "En privat kommentar er ikke synlig for brugeren, mens en offentlig kommentar vises som et værktøjstip, når den svæver i brugeroversigten", - "custom_params": "Brugerdefinerede parametre", - "custom_params_hint": "Rigtigt: --param=xy, forkert: --param xy", - "delete1": "Slet fra kilden, når den er afsluttet", - "delete2": "Slet meddelelser på destinationen, der ikke er fra kilden", - "delete2duplicates": "Slet duplikater på destinationen", "description": "Beskrivelse", "destination": "Bestemmelsessted", "disable_login": "Tillad ikke login (indgående mail accepteres stadig)", @@ -57,7 +51,6 @@ "domain_matches_hostname": "Domæne %s matcher værtsnavn", "domain_quota_m": "Samlet domænekvote (Mb)", "enc_method": "Krypteringsmetode", - "exclude": "Ekskluder objekter (regex)", "full_name": "Fulde navn", "gal": "Global adresseliste", "gal_info": "GAL indeholder alle objekter i et domæne og kan ikke redigeres af nogen bruger. Information om ledig / optaget i SOGo mangler, hvis deaktiveret! Genstart SOGo for at anvende ændringer. ", @@ -73,7 +66,6 @@ "mailbox_username": "Brugernavn (venstre del af en e-mail-adressen)", "max_aliases": "Maks. mulige aliasser", "max_mailboxes": "Maks. mulige postkasser", - "mins_interval": "Afstemningsinterval (minuter)", "multiple_bookings": "Flere bookinger", "nexthop": "Næste hop", "password": "Kodeord", @@ -93,15 +85,9 @@ "select_domain": "Vælg først et domæne", "sieve_desc": "Kort beskrivelse", "sieve_type": "Filtertype", - "skipcrossduplicates": "Spring duplikatbeskeder over mapper (først til mølle)", - "subscribeall": "Abonner på alle mapper", - "syncjob": "Tilføj synkroniseringsjob", - "syncjob_hint": "Vær opmærksom på, at adgangskoder skal gemmes som almindelig tekst!", "target_address": "Gå til adresser", "target_address_info": "Fuld e-mail-adresser (komma-adskilt).", "target_domain": "Måldomæne", - "timeout1": "Timeout for forbindelse til ekstern vært", - "timeout2": "Timeout for forbindelse til lokal vært", "username": "Brugernavn", "validate": "Bekræft", "validation_success": "Valideret med succes", @@ -472,15 +458,11 @@ "allowed_protocols": "Tilladte protokoller", "app_name": "Appens navn", "app_passwd": "App-adgangskode", - "automap": "Prøv at automatisere mapper (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relæindstillinger", "bcc_dest_format": "BCC-destination skal være en enkelt gyldig e-mail-adresse.", "client_id": "Klient-id", "client_secret": "Klienthemmelighed", "comment_info": "En privat kommentar er ikke synlig for brugeren, mens en offentlig kommentar vises som værktøjstip, når den svæver i en brugeroversigt", - "delete1": "Slet fra kilden, når den er afsluttet", - "delete2": "Slet meddelelser på destinationen, der ikke er fra kilden", - "delete2duplicates": "Slet duplikater på destinationen", "delete_ays": "Bekræft sletningsprocessen.", "description": "Beskrivelse", "disable_login": "Tillad ikke login (indgående mail accepteres stadig)", @@ -491,7 +473,6 @@ "dont_check_sender_acl": "Deaktiver afsenderkontrol for domæne %s (+ alias-domæner)", "edit_alias_domain": "Rediger Alias-domæne", "encryption": "Kryptering", - "exclude": "Ekskluder objekter (regex)", "extended_sender_acl": "Eksterne afsenderadresser", "extended_sender_acl_info": "En DKIM-domænenøgle skal importeres, hvis den er tilgængelig.
\r\n Husk at tilføje denne server til den tilsvarende SPF TXT-post.
\r\n Når et domæne eller aliasdomæne føjes til denne server, der overlapper en ekstern adresse, fjernes den eksterne adresse.
\r\n Brug @domain.tld til at tillade at sende som *@domæne.tld.", "force_pw_update": "Tving adgangskodeopdatering til næste login", @@ -509,10 +490,7 @@ "max_aliases": "Maks. aliaser", "max_mailboxes": "Maks. mulige postkasser", "max_quota": "Maks. kvote pr. postkasse (MiB)", - "maxage": "Maksimumalder for beskeder i dage, der vil blive pollet fra fjernbetjeningen
(0 = ignorere alder)", - "maxbytespersecond": "Maks. bytes pr. sekund
(0 = ubegrænset)", "mbox_rl_info": "Denne satsgrænse anvendes på SASL-loginnavnet, den matcher ethvert \"from\" adresse, der bruges af den indloggede bruger. En grænse for sats for postkasse tilsidesætter en satsgrænse for hele domænet.", - "mins_interval": "Interval (min)", "multiple_bookings": "Flere bookinger", "nexthop": "Næste hop", "password": "Adgangskode", @@ -546,18 +524,13 @@ "sender_acl_info": "Hvis postkassebruger A har tilladelse til at sende som postkassebruger B, vises afsenderadressen ikke automatisk som valgbar \"from\" felt i SOGo.
\r\n Postkassebruger B skal oprette en delegation i SOGo for at tillade postkassebruger A at vælge deres adresse som afsender. For at delegere en postkasse i SOGo skal du bruge menuen (tre prikker) til højre for dit postkassens navn øverst til venstre, mens du er i postvisningen. Denne adfærd gælder ikke for aliasadresser.", "sieve_desc": "Kort beskrivelse", "sieve_type": "Filtertype", - "skipcrossduplicates": "Spring duplikatbeskeder over mapper (først til mølle)", "sogo_visible": "Alias er synligt i SOGo", "sogo_visible_info": "Denne indstilling påvirker kun objekter, der kan vises i SOGo (delte eller ikke-delte aliasadresser, der peger på mindst en lokal postkasse). Hvis skjult, vises et alias ikke som valgbar afsender i SOGo.", "spam_alias": "Opret eller skift tidsbegrænsede aliasadresser", "spam_policy": "Tilføj eller fjern genstande til hvid- / sortliste", "spam_score": "Indstil en brugerdefineret spam-score", - "subfolder2": "Synkroniser til undermappe på destinationen
(tom = brug ikke undermappe)", - "syncjob": "Rediger synkroniseringsjob", "target_address": "Gå til adresse (r) (komma separeret)", "target_domain": "Måldomæne", - "timeout1": "Timeout for forbindelse til ekstern vært", - "timeout2": "Timeout for forbindelse til lokal vært", "title": "Rediger objekt", "unchanged_if_empty": "Lad være tomt, hvis uændret", "username": "Brugernavn", @@ -731,7 +704,6 @@ "spam_aliases": "Temp. alias", "stats": "Statistikker", "status": "Status", - "sync_jobs": "Synkroniser job", "table_size": "Bordstørrelse", "table_size_show_n": "Vis %s genstande", "target_address": "Gå til adresse", @@ -939,6 +911,27 @@ "fido2_success": "Enheden blev registreret", "fido2_validation_failed": "Validering mislykkedes" }, + "syncjobs": { + "sync_jobs": "Sync jobs", + "add": "Tilføj synkroniseringsjob", + "add_hint": "Vær opmærksom på, at adgangskoder skal gemmes som almindelig tekst!", + "edit": "Rediger synkroniseringsjob", + "create": "Opret nyt sync job", + "subfolder2": "Synkroniser til undermappe på destinationen
(tom = brug ikke undermappe)", + "mins_interval": "Afstemningsinterval (minuter)", + "maxage": "Maksimumalder for beskeder i dage, der vil blive pollet fra fjernbetjeningen
(0 = ignorere alder)", + "exclude": "Ekskluder objekter (regex)", + "automap": "Prøv at automatisere mapper (\"Sendte emner\", \"Sendt\" => \"Sendt\" etc.)", + "delete1": "Slet fra kilden, når den er afsluttet", + "delete2": "Slet meddelelser på destinationen, der ikke er fra kilden", + "delete2duplicates": "Slet duplikater på destinationen", + "skipcrossduplicates": "Spring duplikatbeskeder over mapper (først til mølle)", + "subscribeall": "Abonner på alle mapper", + "timeout1": "Timeout for forbindelse til ekstern vært", + "timeout2": "Timeout for forbindelse til lokal vært", + "maxbytespersecond": "Maks. bytes pr. sekund
(0 = ubegrænset)", + "custom_params": "Brugerdefinerede parametre" + }, "user": { "action": "Handling", "active": "Aktiv", @@ -963,7 +956,6 @@ "change_password": "Skift kodeord", "client_configuration": "Vis konfigurationsvejledning for email klinter og mobiltelefoner", "create_app_passwd": "Opret app kodeord", - "create_syncjob": "Opret nyt sync job", "daily": "Dagligt", "day": "dag", "description": "Beskrivelse", @@ -1048,7 +1040,6 @@ "spamfilter_wl_desc": "Hvidlistede e-mail-adresser til aldrig at klassificeres som spam. Wildcards kan bruges. Et filter anvendes kun på direkte aliaser (aliaser med en enkelt målpostkasse) eksklusive catch-aliaser og selve en postkasse.", "spamfilter_yellow": "Gul: denne besked kan være spam, vil blive tagget som spam og flyttes til din junk-mappe", "status": "Status", - "sync_jobs": "Sync jobs", "tag_handling": "Set handling for taggede mails", "tag_help_example": "Eksempel på en tagget e-mail-adresse : me+Tacaly@example.org", "tag_help_explain": "I undermappe: En ny undermappe opkaldt efter tagget oprettes under INBOX (\"INBOX/Tacaly\").
\r\nI emne: navnene på tags bliver præpenderet til mailsemnet, eksempel: \"[Tacaly] Mine Nyhedder\".", diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 67f78e049..29919b71b 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -28,7 +28,7 @@ "spam_alias": "Temporäre E-Mail-Aliasse", "spam_policy": "Deny/Allowlist", "spam_score": "Spam-Bewertung", - "syncjobs": "Sync Jobs", + "syncjobs": "Sync-Jobs", "tls_policy": "Verschlüsselungsrichtlinie", "unlimited_quota": "Unendliche Quota für Mailboxen" }, @@ -45,24 +45,16 @@ "app_name": "App-Name", "app_password": "App-Passwort hinzufügen", "app_passwd_protocols": "Zugelassene Protokolle für App-Passwort", - "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay-Optionen", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", "comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.", - "custom_params": "Eigene Parameter", - "custom_params_hint": "Richtig: --param=xy, falsch: --param xy", - "delete1": "Lösche Nachricht nach Übertragung vom Quell-Server.", - "delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf dem Quell-Server vorhanden sind.", - "delete2duplicates": "Lösche Duplikate im Ziel", "description": "Beschreibung", "destination": "Ziel", "disable_login": "Login verbieten (Mails werden weiterhin angenommen)", "domain": "Domain", "domain_matches_hostname": "Domain %s darf nicht dem Hostnamen entsprechen", "domain_quota_m": "Domain-Speicherplatz gesamt (MiB)", - "dry": "Synchronisation simulieren", "enc_method": "Verschlüsselung", - "exclude": "Elemente ausschließen (Regex)", "full_name": "Vor- und Nachname", "gal": "Globales Adressbuch", "gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! Zum Anwenden einer Änderung muss SOGo neugestartet werden.", @@ -81,7 +73,6 @@ "mailbox_username": "Benutzername (linker Teil der E-Mail-Adresse)", "max_aliases": "Max. mögliche Aliasse", "max_mailboxes": "Max. mögliche Mailboxen", - "mins_interval": "Abrufintervall (Minuten)", "multiple_bookings": "Mehrfaches Buchen möglich", "nexthop": "Next Hop", "password": "Passwort", @@ -101,15 +92,9 @@ "select_domain": "Bitte zuerst eine Domain auswählen", "sieve_desc": "Kurze Beschreibung", "sieve_type": "Filtertyp", - "skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")", - "subscribeall": "Alle synchronisierten Ordner abonnieren", - "syncjob": "Sync-Job hinzufügen", - "syncjob_hint": "Passwörter werden unverschlüsselt abgelegt!", "target_address": "Ziel-Adresse(n)", "target_address_info": "Vollständige E-Mail-Adresse(n). Getrennt durch Komma.", "target_domain": "Ziel-Domain", - "timeout1": "Timeout für Verbindung zum Remote-Host", - "timeout2": "Timeout für Verbindung zum lokalen Host", "username": "Benutzername", "validate": "Validieren", "validation_success": "Erfolgreich validiert", @@ -418,7 +403,12 @@ "logo_dark_label": "Invertiert für den Darkmode", "logo_normal_label": "Normal", "user_link": "Nutzer-Link", - "filter": "Filter" + "filter": "Filter", + "syncjobs": "Sync-Jobs", + "syncjob_max_parallel": "Max. parallele Sync-Prozesse", + "syncjob_max_parallel_info": "Wie viele Sync-Jobs gleichzeitig laufen dürfen (1-32).", + "syncjob_max_kb_per_second": "Bandbreiten-Limit pro Prozess (KB/s)", + "syncjob_max_kb_per_second_info": "0 = unbegrenzt. Begrenzt jeden Sync-Prozess in KB/s; ein Job-Limit (Bytes/s) wird hierdurch zusätzlich begrenzt. Gesamt ≈ dieser Wert × parallele Prozesse." }, "danger": { "access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten", @@ -567,7 +557,31 @@ "template_exists": "Vorlage %s existiert bereits", "template_id_invalid": "Vorlagen-ID %s ungültig", "template_name_invalid": "Name der Vorlage ungültig", - "required_data_missing": "Die benötigten Daten: %s fehlen" + "required_data_missing": "Die benötigten Daten: %s fehlen", + "imapsync_source_in_use": "Sync-Quelle %s wird noch von einem Sync-Job verwendet und kann nicht gelöscht werden", + "imapsync_source_token_refresh_failed": "OAuth-Token-Erneuerung für %s fehlgeschlagen", + "imapsync_source_scope_empty": "Für die gewählte Sichtbarkeit mindestens eine Domain wählen", + "imapsync_source_unavailable": "Die gewählte Sync-Quelle ist nicht verfügbar", + "imapsync_source_oauth_connect_required": "Vor dem Speichern dieses Sync-Jobs beim Provider anmelden", + "imapsync_source_name_invalid": "Ungültiger Name der Sync-Quelle", + "imapsync_source_host_invalid": "Ungültiger Hostname oder ungültige IP-Adresse", + "imapsync_source_port_invalid": "Ungültiger Port", + "imapsync_source_enc_invalid": "Ungültige Verschlüsselungsmethode", + "imapsync_source_auth_invalid": "Ungültige Authentifizierungsmethode", + "imapsync_custom_param_invalid": "Eigene Parameter enthalten eine nicht erlaubte imapsync-Option: %s", + "imapsync_source_oauth_required": "OAuth2 erfordert Token-Endpunkt, Client-ID, Client-Secret und Scope", + "imapsync_source_oauth_endpoint_invalid": "Ungültiger OAuth2-Endpunkt: Erforderlich ist eine https://-URL mit gültigem Hostnamen (Quellen, die nicht von einem Administrator angelegt wurden, brauchen einen öffentlich erreichbaren Host)", + "imapsync_source_oauth_extra_invalid": "OAuth2-Extra-Parameter müssen ein JSON-Objekt mit einfachen Werten sein, z. B. {\"resource\": \"https://example.com\"}", + "imapsync_source_tenant_missing": "Bitte den Microsoft-365-Tenant angeben", + "imapsync_source_oauth_invalid": "Ungültige OAuth2-Konfiguration", + "imapsync_max_parallel_invalid": "Die Anzahl paralleler Prozesse muss zwischen 1 und 32 liegen", + "imapsync_source_description_invalid": "Die Beschreibung darf höchstens 255 Zeichen lang sein", + "imapsync_source_refresh_unsupported": "Die manuelle Token-Erneuerung ist nur für Quellen mit Client-Credentials verfügbar", + "imapsync_custom_params_format": "Eigene Parameter müssen als Liste von Option/Wert-Paaren übermittelt werden", + "imapsync_custom_param_value": "Ungültiger Wert für die imapsync-Option %s (Wert fehlt, Wert bei einem Schalter oder nicht erlaubter Wert)", + "imapsync_job_running": "Sync-Job %s läuft gerade und kann nicht als Nächstes eingeplant werden", + "imapsync_user1_invalid": "Ungültiger Benutzername an der Quelle", + "imapsync_max_kb_per_second_invalid": "Das Bandbreitenlimit muss zwischen 0 und 10485760 KB/s liegen" }, "datatables": { "collapse_all": "Alle Einklappen", @@ -656,7 +670,6 @@ "app_name": "App-Name", "app_passwd": "App-Passwörter", "app_passwd_protocols": "Zugelassene Protokolle für App-Passwort", - "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay-Optionen", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", "client_id": "Client-ID", @@ -664,9 +677,6 @@ "comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.", "created_on": "Erstellt am", "custom_attributes": "benutzerdefinierte Attribute", - "delete1": "Lösche Nachricht nach Übertragung vom Quell-Server", - "delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind", - "delete2duplicates": "Lösche Duplikate im Ziel", "delete_ays": "Bitte den Löschvorgang bestätigen.", "description": "Beschreibung", "disable_login": "Login verbieten (Mails werden weiterhin angenommen)", @@ -690,7 +700,6 @@ "dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen", "edit_alias_domain": "Alias-Domain bearbeiten", "encryption": "Verschlüsselung", - "exclude": "Elemente ausschließen (Regex)", "extended_sender_acl": "Externe Absenderadressen", "extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.
\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.
\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.
\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld", "force_pw_update": "Erzwinge Passwortänderung bei nächstem Login", @@ -721,10 +730,7 @@ "max_aliases": "Max. Aliasse", "max_mailboxes": "Max. Mailboxanzahl", "max_quota": "Max. Größe per Mailbox (MiB)", - "maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll
(0 = alle Nachrichten kopieren)", - "maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)", "mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.", - "mins_interval": "Intervall (min)", "mta_sts": "MTA-STS", "mta_sts_info": "MTA-STS ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden.
Er wird verwendet, wenn DANE aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.
Hinweis: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE immer bevorzugt – MTA-STS fungiert nur als Fallback.", "mta_sts_version": "Version", @@ -776,7 +782,6 @@ "sender_acl_info": "Wird einem Mailbox-Benutzer A der Versand als Mailbox-Benutzer B gestattet, so erscheint der Absender nicht automatisch in SOGo zur Auswahl.
\r\n In SOGo muss zusätzlich eine Delegation eingerichtet werden. Dieses Verhalten trifft nicht auf Alias-Adressen zu.", "sieve_desc": "Kurze Beschreibung", "sieve_type": "Filtertyp", - "skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")", "sogo_access": "Direktes weiterleiten an SOGo", "sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.", "sogo_visible": "Alias in SOGo sichtbar", @@ -785,12 +790,8 @@ "spam_filter": "Spamfilter", "spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten", "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen", - "subfolder2": "Ziel-Ordner
(leer = kein Unterordner)", - "syncjob": "Sync-Job bearbeiten", "target_address": "Ziel-Adresse(n)(kommasepariert)", "target_domain": "Ziel-Domain", - "timeout1": "Timeout für Verbindung zum Remote-Host", - "timeout2": "Timeout für Verbindung zum lokalen Host", "title": "Objekt bearbeiten", "unchanged_if_empty": "Unverändert, wenn leer", "username": "Benutzername", @@ -1004,7 +1005,6 @@ "spam_aliases": "Spam-Alias", "stats": "Statistik", "status": "Status", - "sync_jobs": "Synchronisationen", "table_size": "Tabellengröße", "table_size_show_n": "Zeige %s Einträge", "target_address": "Ziel-Adresse", @@ -1032,17 +1032,8 @@ "open_logs": "Öffne Logs", "recipient": "Empfänger", "sender": "Sender", - "syncjob_EX_OK": "Erfolg", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort", - "syncjob_check_log": "Logs überprüfen", "all_domains": "Alle Domains", "catch_all": "Catch-All", - "syncjob_last_run_result": "Letztes Ausführungsergebnis", - "syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem", - "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", - "syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen", "iam": "Identitätsanbieter", "force_tfa": "2-Faktor-Authentifizierung" }, @@ -1218,7 +1209,13 @@ "verified_fido2_login": "FIDO2-Anmeldung verifiziert", "verified_totp_login": "TOTP-Anmeldung verifiziert", "verified_webauthn_login": "WebAuthn-Anmeldung verifiziert", - "verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert" + "verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert", + "imapsync_source_added": "Sync-Quelle %s wurde hinzugefügt", + "imapsync_source_modified": "Sync-Quelle %s wurde gespeichert", + "imapsync_source_deleted": "Sync-Quelle %s wurde gelöscht", + "imapsync_source_token_refreshed": "OAuth-Token für %s wurde erneuert", + "max_parallel_saved": "Sync-Job-Einstellungen gespeichert", + "imapsync_run_next_done": "%s Sync-Job(s) an den Anfang der Warteschlange gesetzt" }, "tfa": { "authenticators": "Authentikatoren", @@ -1253,6 +1250,130 @@ "setup_required": "Ihr Konto erfordert Zwei-Faktor-Authentifizierung. Bitte richten Sie eine 2FA-Methode ein, um fortzufahren.", "cancel_setup": "Abbrechen und abmelden" }, + "syncjobs": { + "sources_panel": "Sync-Quellen", + "sources_hint": "Sync-Quellen definieren IMAP-Server (mit ihrer Authentifizierungsmethode), die von Sync-Jobs wiederverwendet werden können. Jede Quelle wird einmal angelegt und dann beim Anlegen eines Sync-Jobs aus dem Dropdown ausgewählt.", + "source_add": "Quelle hinzufügen", + "source": "Quelle", + "source_name": "Name", + "source_description": "Beschreibung", + "source_scope": "Sichtbarkeit", + "source_scope_all": "Alle Benutzer", + "source_scope_domain": "Bestimmte Domains", + "source_scope_user": "Bestimmte Benutzer", + "source_domains": "Domains", + "source_users": "Benutzer", + "source_select": "Quelle wählen", + "source_auth_type": "Authentifizierung", + "source_oauth_block": "OAuth2-Konfiguration", + "source_token_status": "Token gültig bis", + "source_token_error": "Letzter Refresh-Fehler", + "source_refresh_now": "Token jetzt erneuern", + "source_secret_unchanged_hint": "Leer lassen, um das gespeicherte Secret beizubehalten.", + "source_oauth_flow": "OAuth2-Flow", + "source_oauth_flow_app": "App (Client Credentials)", + "source_oauth_flow_user": "Benutzer-Login (Authorization Code)", + "source_flow_badge_app": "Client Credentials", + "source_flow_badge_user": "Benutzer-Login", + "token_state_issued": "Token ausgestellt", + "token_state_pending": "Anmeldung ausstehend", + "token_state_failed": "Token fehlgeschlagen", + "source_authorize_endpoint": "Autorisierungs-Endpunkt", + "source_userinfo_endpoint": "Userinfo-Endpunkt", + "source_redirect_uri": "Redirect-URI", + "source_redirect_uri_hint": "Diese URI in der Provider-App (z. B. Azure) eintragen. Die Anmeldung beim Provider funktioniert nur, wenn mailcow über diesen Hostnamen aufgerufen wird.", + "syncjob_oauth_connect": "Beim Provider anmelden", + "syncjob_oauth_connected": "Verbunden", + "sync_jobs": "Sync-Jobs", + "syncjob_check_log": "Logs überprüfen", + "syncjob_last_run_result": "Letztes Ausführungsergebnis", + "syncjob_EX_OK": "Erfolg", + "syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem", + "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", + "syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Keine Verbindung zum Quellserver möglich", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort", + "syncjob_OAUTH_TOKEN_NOT_READY": "OAuth-Token nicht bereit (erneut anmelden oder auf die Token-Erneuerung warten)", + "add": "Sync-Job hinzufügen", + "add_hint": "Passwörter werden unverschlüsselt abgelegt!", + "edit": "Sync-Job bearbeiten", + "create": "Neuen Sync-Job erstellen", + "dry": "Synchronisation simulieren", + "subfolder2": "Ziel-Ordner
(leer = kein Unterordner)", + "mins_interval": "Abrufintervall (Minuten)", + "maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll
(0 = alle Nachrichten kopieren)", + "exclude": "Elemente ausschließen (Regex)", + "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", + "delete1": "Nachrichten nach der Übertragung auf dem Quellserver löschen", + "delete2": "Nachrichten im Ziel löschen, die auf dem Quellserver nicht vorhanden sind", + "delete2duplicates": "Lösche Duplikate im Ziel", + "skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")", + "subscribeall": "Alle synchronisierten Ordner abonnieren", + "timeout1": "Timeout für die Verbindung zum Quellserver", + "timeout2": "Timeout für die Verbindung zum lokalen Server", + "maxbytespersecond": "Max. Übertragungsrate in Bytes/s
(0 = unbegrenzt, zusätzlich begrenzt durch das globale Limit pro Prozess in KB/s)", + "custom_params": "Eigene Parameter", + "custom_params_hint": "Pro Zeile eine imapsync-Option. Es werden nur erlaubte Optionen akzeptiert; der Wert darf beliebige Zeichen enthalten (Leerzeichen, Regex, …) und wird als ein einzelnes Argument übergeben.", + "custom_param_option": "Option", + "custom_param_value": "Wert", + "custom_param_no_value": "kein Wert nötig", + "custom_param_add": "Parameter hinzufügen", + "prio": "Priorität", + "prio_hint": "Jobs mit höherer Priorität laufen zuerst. Bei gleicher Priorität kommen noch nie gelaufene Jobs zuerst, danach die am längsten nicht gelaufenen (0 = normal).", + "oauth_popup_title": "Beim Provider anmelden", + "oauth_popup_success": "Verbunden. Dieses Fenster kann geschlossen werden.", + "oauth_popup_failed": "Anmeldung fehlgeschlagen. Dieses Fenster kann geschlossen werden.", + "oauth_error_access_denied": "Sie dürfen keine Sync-Jobs verwalten", + "oauth_error_mailbox_denied": "Kein Zugriff auf die gewählte lokale Mailbox", + "oauth_error_source_unavailable": "Die Sync-Quelle ist nicht verfügbar oder nicht für die Anmeldung beim Provider eingerichtet", + "oauth_error_provider_denied": "Der Provider hat die Anmeldung abgelehnt", + "oauth_error_invalid_state": "Die Anmeldesitzung ist abgelaufen oder ungültig, bitte erneut versuchen", + "oauth_error_token_failed": "Der Provider hat kein Token ausgestellt (Client-ID, Secret und Redirect-URI prüfen)", + "oauth_error_no_identity": "Der Provider hat keine E-Mail-Adresse für das angemeldete Konto geliefert", + "user2_label": "Lokale Mailbox (Ziel)", + "user1_label": "Benutzername an der Quelle", + "password1_label": "Passwort an der Quelle", + "no_sources": "Keine Sync-Quelle verfügbar. Bitte zuerst eine Sync-Quelle anlegen.", + "source_list_error": "Sync-Quellen konnten nicht geladen werden", + "source_not_visible": "(nicht sichtbar)", + "oauth_waiting": "Warte auf den Provider…", + "oauth_popup_blocked": "Das Anmeldefenster wurde blockiert. Bitte Pop-ups für diese Seite erlauben und erneut versuchen.", + "oauth_select_mailbox": "Bitte zuerst die lokale Mailbox wählen", + "oauth_error_unknown": "Anmeldung fehlgeschlagen", + "oauth_hint": "Der Benutzername an der Quelle wird aus dem Konto übernommen, mit dem die Anmeldung beim Provider erfolgt.", + "token_state_none": "Token ausstehend", + "custom_params_legacy": "Alt-Parameter (nicht übernommen)", + "custom_params_legacy_hint": "Diese eigenen Parameter des ursprünglichen Sync-Jobs konnten nicht übernommen werden und werden ignoriert. Noch benötigte Optionen bitte oben im Parameter-Editor neu anlegen.", + "custom_params_legacy_badge": "Alt-Parameter", + "custom_param_remove": "Parameter entfernen", + "never_run": "Noch nie", + "status_inactive": "Inaktiv", + "source_server": "Quellserver", + "id": "ID", + "source_created_by": "Erstellt von", + "source_oauth_token_endpoint": "Token-Endpunkt", + "source_oauth_client_id": "Client-ID", + "source_oauth_client_secret": "Client-Secret", + "source_oauth_scope": "Scope", + "source_oauth_extra_params": "Zusätzliche Parameter (JSON)", + "source_oauth_extra_params_hint": "Optionales JSON-Objekt. prompt, access_type, login_hint, domain_hint und hd gehen an die Anmeldung beim Provider, alle anderen an die Token-Anfrage, z. B. {\"resource\": \"https://provider.example.com\"}", + "source_presets": "Vorlagen", + "source_presets_hint": "Füllt die Serverdaten des Anbieters aus. Viele Anbieter (z. B. Gmail, iCloud, Yahoo) verlangen für IMAP ein App-Passwort oder die Freischaltung von IMAP in den Postfach-Einstellungen.", + "source_tenant": "Microsoft-365-Tenant", + "source_tenant_hint": "Verzeichnis-ID (Mandant) oder eine Domain des Tenants, z. B. contoso.onmicrosoft.com. Wird in die Endpunkte eingesetzt.", + "source_scope_cc_hint": "Quellen mit Client-Credentials-Flow sind immer nur für ihren Ersteller sichtbar.", + "source_readonly_hint": "Diese Sync-Quelle ist freigegeben und kann nur angesehen werden.", + "copy": "Kopieren", + "copied": "In die Zwischenablage kopiert", + "source_view": "Anzeigen", + "custom_params_options_error": "Die Liste der erlaubten imapsync-Optionen konnte nicht geladen werden", + "source_missing": "Diesem Sync-Job ist keine Sync-Quelle zugeordnet. Bitte eine Quelle wählen.", + "oauth_window_closed": "Das Anmeldefenster wurde geschlossen, bevor ein Ergebnis ankam. Falls die Anmeldung abgeschlossen wurde, die Seite neu laden oder es erneut versuchen.", + "custom_params_legacy_badge_title": "Nicht übernommene alte Parameter werden ignoriert. Details auf der Bearbeiten-Seite.", + "status_waiting": "Wartend", + "status_running": "Wird ausgeführt" + }, "user": { "action": "Aktion", "active": "Aktiv", @@ -1283,7 +1404,6 @@ "clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen", "client_configuration": "Konfigurationsanleitungen für E-Mail-Programme und Smartphones anzeigen", "create_app_passwd": "Erstelle App-Passwort", - "create_syncjob": "Neuen Sync-Job erstellen", "created_on": "Erstellt am", "daily": "Täglich", "day": "Tag", @@ -1391,7 +1511,6 @@ "spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter nicht erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-„Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.", "spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben", "status": "Status", - "sync_jobs": "Sync Jobs", "tag_handling": "Umgang mit getaggten E-Mails steuern", "tag_help_example": "Beispiel für eine getaggte E-Mail-Adresse: ich+Facebook@example.org", "tag_help_explain": "Als Unterordner: Es wird ein Ordner mit dem Namen des Tags unterhalb der Inbox erstellt (\"INBOX/Facebook\").
\r\nIn Betreff: Der Name des Tags wird dem Betreff angefügt, etwa \"[Facebook] Meine Neuigkeiten\".", @@ -1416,16 +1535,7 @@ "with_app_password": "mit App-Passwort", "year": "Jahr", "years": "Jahren", - "syncjob_EX_OK": "Erfolg", "open_logs": "Öffne Logs", - "syncjob_check_log": "Logs überprüfen", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen", - "syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit", - "syncjob_last_run_result": "Letztes Ausführungsergebnis", - "syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem", - "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort", "pushover_sound": "Ton" }, "warning": { diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 1f1c1f273..16fc055eb 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -45,24 +45,16 @@ "app_name": "App name", "app_password": "Add app password", "app_passwd_protocols": "Allowed protocols for app password", - "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay options", "bcc_dest_format": "BCC destination must be a single valid email address.
If you need to send a copy to multiple addresses, create an alias and use it here.", "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview", - "custom_params": "Custom parameters", - "custom_params_hint": "Right: --param=xy, wrong: --param xy", - "delete1": "Delete from source when completed", - "delete2": "Delete messages on destination that are not on source", - "delete2duplicates": "Delete duplicates on destination", "description": "Description", "destination": "Destination", "disable_login": "Disallow login (incoming mail is still accepted)", "domain": "Domain", "domain_matches_hostname": "Domain %s matches hostname", "domain_quota_m": "Total domain quota (MiB)", - "dry": "Simulate synchronization", "enc_method": "Encryption method", - "exclude": "Exclude objects (regex)", "full_name": "Full name", "gal": "Global Address List", "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! Restart SOGo to apply changes.", @@ -81,7 +73,6 @@ "mailbox_username": "Username (left part of an email address)", "max_aliases": "Max. possible aliases", "max_mailboxes": "Max. possible mailboxes", - "mins_interval": "Polling interval (minutes)", "multiple_bookings": "Multiple bookings", "nexthop": "Next hop", "password": "Password", @@ -101,16 +92,10 @@ "select_domain": "Please select a domain first", "sieve_desc": "Short description", "sieve_type": "Filter type", - "skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)", - "subscribeall": "Subscribe all folders", - "syncjob": "Add sync job", - "syncjob_hint": "Be aware that passwords need to be saved plain-text!", "tags": "Tags", "target_address": "Goto addresses", "target_address_info": "Full email address/es (comma-separated).", "target_domain": "Target domain", - "timeout1": "Timeout for connection to remote host", - "timeout2": "Timeout for connection to local host", "username": "Username", "validate": "Validate", "validation_success": "Validated successfully" @@ -418,7 +403,12 @@ "user_quicklink": "Hide Quicklink to User Login Page", "validate_license_now": "Validate GUID against license server", "verify": "Verify", - "yes": "✓" + "yes": "✓", + "syncjobs": "Sync jobs", + "syncjob_max_parallel": "Max. parallel sync processes", + "syncjob_max_parallel_info": "How many sync jobs may run at the same time (1-32).", + "syncjob_max_kb_per_second": "Bandwidth limit per process (KB/s)", + "syncjob_max_kb_per_second_info": "0 = unlimited. Caps every sync process in KB/s; a per-job limit (bytes/s) is capped by this. Total ≈ this × max parallel." }, "danger": { "access_denied": "Access denied or invalid form data", @@ -567,7 +557,31 @@ "validity_missing": "Please assign a period of validity", "value_missing": "Please provide all values", "version_invalid": "Version %s is invalid", - "yotp_verification_failed": "Yubico OTP verification failed: %s" + "yotp_verification_failed": "Yubico OTP verification failed: %s", + "imapsync_source_in_use": "Sync source %s is still used by a sync job and cannot be deleted", + "imapsync_source_token_refresh_failed": "OAuth token refresh for %s failed", + "imapsync_source_scope_empty": "Select at least one domain for the chosen visibility", + "imapsync_source_unavailable": "The selected sync source is unavailable", + "imapsync_source_oauth_connect_required": "Sign in with the provider before saving this sync job", + "imapsync_source_name_invalid": "Invalid sync source name", + "imapsync_source_host_invalid": "Invalid host name or IP address", + "imapsync_source_port_invalid": "Invalid port", + "imapsync_source_enc_invalid": "Invalid encryption method", + "imapsync_source_auth_invalid": "Invalid authentication method", + "imapsync_custom_param_invalid": "Custom parameters contain a disallowed imapsync option: %s", + "imapsync_source_oauth_required": "OAuth2 requires token endpoint, client ID, client secret and scope", + "imapsync_source_oauth_endpoint_invalid": "Invalid OAuth2 endpoint: an https:// URL with a valid host name is required (sources not created by an administrator need a publicly reachable host)", + "imapsync_source_oauth_extra_invalid": "OAuth2 extra parameters must be a JSON object with simple values, e.g. {\"resource\": \"https://example.com\"}", + "imapsync_source_tenant_missing": "Please enter the Microsoft 365 tenant", + "imapsync_source_oauth_invalid": "Invalid OAuth2 configuration", + "imapsync_max_parallel_invalid": "Parallel process count must be between 1 and 32", + "imapsync_source_description_invalid": "The description must not exceed 255 characters", + "imapsync_source_refresh_unsupported": "Manual token refresh is only available for client credentials sources", + "imapsync_custom_params_format": "Custom parameters must be sent as a list of option/value pairs", + "imapsync_custom_param_value": "Invalid value for imapsync option %s (missing value, value on a flag or disallowed value)", + "imapsync_job_running": "Sync job %s is currently running and cannot be scheduled next", + "imapsync_user1_invalid": "Invalid remote username", + "imapsync_max_kb_per_second_invalid": "Bandwidth limit must be between 0 and 10485760 KB/s" }, "datatables": { "collapse_all": "Collapse All", @@ -656,7 +670,6 @@ "app_name": "App name", "app_passwd": "App password", "app_passwd_protocols": "Allowed protocols for app password", - "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay options", "bcc_dest_format": "BCC destination must be a single valid email address.
If you need to send a copy to multiple addresses, create an alias and use it here.", "client_id": "Client ID", @@ -664,9 +677,6 @@ "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview", "created_on": "Created on", "custom_attributes": "Custom attributes", - "delete1": "Delete from source when completed", - "delete2": "Delete messages on destination that are not on source", - "delete2duplicates": "Delete duplicates on destination", "delete_ays": "Please confirm the deletion process.", "description": "Description", "disable_login": "Disallow login (incoming mail is still accepted)", @@ -690,7 +700,6 @@ "dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)", "edit_alias_domain": "Edit Alias domain", "encryption": "Encryption", - "exclude": "Exclude objects (regex)", "extended_sender_acl": "External sender addresses", "extended_sender_acl_info": "A DKIM domain key should be imported, if available.
\r\n Remember to add this server to the corresponding SPF TXT record.
\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.
\r\n Use @domain.tld to allow to send as *@domain.tld.", "force_pw_update": "Force password update at next login", @@ -721,10 +730,7 @@ "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", - "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", - "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", "mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.", - "mins_interval": "Interval (min)", "mta_sts": "MTA-STS", "mta_sts_info": "MTA-STS is a standard that enforces email delivery between mail servers to use TLS with valid certificates.
It is used when DANE is not possible due to missing or unsupported DNSSEC.
Note: If the receiving domain supports DANE with DNSSEC, DANE is always preferred – MTA-STS only acts as a fallback.", "mta_sts_version": "Version", @@ -777,7 +783,6 @@ "sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.
\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.", "sieve_desc": "Short description", "sieve_type": "Filter type", - "skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)", "sogo_access": "Direct forwarding to SOGo", "sogo_access_info": "After logging in, the user is automatically redirected to SOGo.", "sogo_visible": "Alias is visible in SOGo", @@ -786,12 +791,8 @@ "spam_filter": "Spam filter", "spam_policy": "Add or remove items to allow-/denylist", "spam_score": "Set a custom spam score", - "subfolder2": "Sync into subfolder on destination
(empty = do not use subfolder)", - "syncjob": "Edit sync job", "target_address": "Goto address/es (comma-separated)", "target_domain": "Target domain", - "timeout1": "Timeout for connection to remote host", - "timeout2": "Timeout for connection to local host", "title": "Edit object", "unchanged_if_empty": "If unchanged leave blank", "username": "Username", @@ -1013,16 +1014,6 @@ "spam_aliases": "Temp. alias", "stats": "Statistics", "status": "Status", - "sync_jobs": "Sync jobs", - "syncjob_check_log": "Check log", - "syncjob_last_run_result": "Last run result", - "syncjob_EX_OK": "Success", - "syncjob_EXIT_CONNECTION_FAILURE": "Connection problem", - "syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem", - "syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password", "table_size": "Table size", "table_size_show_n": "Show %s items", "target_address": "Goto address", @@ -1225,7 +1216,13 @@ "verified_fido2_login": "Verified FIDO2 login", "verified_totp_login": "Verified TOTP login", "verified_webauthn_login": "Verified WebAuthn login", - "verified_yotp_login": "Verified Yubico OTP login" + "verified_yotp_login": "Verified Yubico OTP login", + "imapsync_source_added": "Sync source %s has been added", + "imapsync_source_modified": "Sync source %s has been saved", + "imapsync_source_deleted": "Sync source %s has been deleted", + "imapsync_source_token_refreshed": "OAuth token for %s has been refreshed", + "max_parallel_saved": "Sync job settings saved", + "imapsync_run_next_done": "%s sync job(s) moved to the front of the queue" }, "tfa": { "authenticators": "Authenticators", @@ -1260,6 +1257,130 @@ "waiting_usb_register": "Waiting for USB device...

Please enter your password above and confirm your registration by tapping the button on your USB device.", "yubi_otp": "Yubico OTP authentication" }, + "syncjobs": { + "sources_panel": "Sync sources", + "sources_hint": "Sync sources define IMAP servers (and their authentication method) that sync jobs can reuse. Define each source once, then pick it from the dropdown when creating sync jobs.", + "source_add": "Add source", + "source": "Source", + "source_name": "Name", + "source_description": "Description", + "source_scope": "Visibility", + "source_scope_all": "All users", + "source_scope_domain": "Specific domains", + "source_scope_user": "Specific users", + "source_domains": "Domains", + "source_users": "Users", + "source_select": "Select source", + "source_auth_type": "Authentication", + "source_oauth_block": "OAuth2 configuration", + "source_token_status": "Token valid until", + "source_token_error": "Last refresh error", + "source_refresh_now": "Refresh token now", + "source_secret_unchanged_hint": "Leave blank to keep the stored secret.", + "source_oauth_flow": "OAuth2 flow", + "source_oauth_flow_app": "App (client credentials)", + "source_oauth_flow_user": "User login (authorization code)", + "source_flow_badge_app": "Client credentials", + "source_flow_badge_user": "User login", + "token_state_issued": "Token issued", + "token_state_pending": "Token pending", + "token_state_failed": "Token failed", + "source_authorize_endpoint": "Authorize endpoint", + "source_userinfo_endpoint": "Userinfo endpoint", + "source_redirect_uri": "Redirect URI", + "source_redirect_uri_hint": "Register this URI in the provider app (e.g. Azure). Provider sign-in only works when mailcow is opened via this host name.", + "syncjob_oauth_connect": "Sign in with the provider", + "syncjob_oauth_connected": "Connected", + "sync_jobs": "Sync jobs", + "syncjob_check_log": "Check log", + "syncjob_last_run_result": "Last run result", + "syncjob_EX_OK": "Success", + "syncjob_EXIT_CONNECTION_FAILURE": "Connection problem", + "syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem", + "syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password", + "syncjob_OAUTH_TOKEN_NOT_READY": "OAuth token not ready (sign in again or wait for the token refresh)", + "add": "Add sync job", + "add_hint": "Be aware that passwords need to be saved plain-text!", + "edit": "Edit sync job", + "create": "Create new sync job", + "dry": "Simulate synchronization", + "subfolder2": "Sync into subfolder on destination
(empty = do not use subfolder)", + "mins_interval": "Polling interval (minutes)", + "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", + "exclude": "Exclude objects (regex)", + "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", + "delete1": "Delete from source when completed", + "delete2": "Delete messages on destination that are not on source", + "delete2duplicates": "Delete duplicates on destination", + "skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)", + "subscribeall": "Subscribe all folders", + "timeout1": "Timeout for connection to remote host", + "timeout2": "Timeout for connection to local host", + "maxbytespersecond": "Max. bandwidth in bytes/s
(0 = unlimited, additionally capped by the global limit per process in KB/s)", + "custom_params": "Custom parameters", + "custom_params_hint": "Add one imapsync option per row. Only allowlisted options are accepted; the value may contain any character (spaces, regex, …) and is passed as a single argument.", + "custom_param_option": "Option", + "custom_param_value": "Value", + "custom_param_no_value": "no value needed", + "custom_param_add": "Add parameter", + "prio": "Priority", + "prio_hint": "Jobs with a higher priority run first. Within the same priority, jobs that never ran come first, then the least recently run ones (0 = normal).", + "oauth_popup_title": "Sign in with the provider", + "oauth_popup_success": "Connected. You can close this window.", + "oauth_popup_failed": "Sign-in failed. You can close this window.", + "oauth_error_access_denied": "You are not allowed to manage sync jobs", + "oauth_error_mailbox_denied": "No access to the selected local mailbox", + "oauth_error_source_unavailable": "The sync source is unavailable or not configured for provider sign-in", + "oauth_error_provider_denied": "The provider refused the sign-in", + "oauth_error_invalid_state": "The sign-in session expired or is invalid, please try again", + "oauth_error_token_failed": "The provider did not issue a token (check client ID, secret and redirect URI)", + "oauth_error_no_identity": "The provider did not return an e-mail address for the signed-in account", + "user2_label": "Local mailbox (destination)", + "user1_label": "Remote username (source)", + "password1_label": "Remote password (source)", + "no_sources": "No sync source is available. Please add a sync source first.", + "source_list_error": "Sync sources could not be loaded", + "source_not_visible": "(not visible)", + "oauth_waiting": "Waiting for the provider…", + "oauth_popup_blocked": "The sign-in window was blocked. Please allow pop-ups for this site and try again.", + "oauth_select_mailbox": "Please select the local mailbox first", + "oauth_error_unknown": "Sign-in failed", + "oauth_hint": "The remote username is taken from the account used to sign in with the provider.", + "token_state_none": "Token pending", + "custom_params_legacy": "Legacy parameters (not migrated)", + "custom_params_legacy_hint": "These custom parameters of the original sync job could not be converted and are ignored. Add the options you still need with the parameter editor above.", + "custom_params_legacy_badge": "Legacy parameters", + "custom_param_remove": "Remove parameter", + "never_run": "Never", + "status_inactive": "Inactive", + "source_server": "Source server", + "id": "ID", + "source_created_by": "Created by", + "source_oauth_token_endpoint": "Token endpoint", + "source_oauth_client_id": "Client ID", + "source_oauth_client_secret": "Client secret", + "source_oauth_scope": "Scope", + "source_oauth_extra_params": "Extra parameters (JSON)", + "source_oauth_extra_params_hint": "Optional JSON object. prompt, access_type, login_hint, domain_hint and hd are sent with the provider sign-in, all others with the token request, e.g. {\"resource\": \"https://provider.example.com\"}", + "source_presets": "Presets", + "source_presets_hint": "Fills in the provider's server settings. Many providers (e.g. Gmail, iCloud, Yahoo) require an app password or IMAP access to be enabled in the mailbox settings.", + "source_tenant": "Microsoft 365 tenant", + "source_tenant_hint": "Directory (tenant) ID or a domain of the tenant, e.g. contoso.onmicrosoft.com. It is inserted into the endpoints.", + "source_scope_cc_hint": "Sources using the client credentials flow are always private to their creator.", + "source_readonly_hint": "This sync source is shared with you and can only be viewed.", + "copy": "Copy", + "copied": "Copied to clipboard", + "source_view": "Show", + "custom_params_options_error": "The list of allowed imapsync options could not be loaded", + "source_missing": "No sync source is assigned to this job. Please select a source.", + "oauth_window_closed": "The sign-in window was closed before a result arrived. If you completed the sign-in, reload the page or try again.", + "custom_params_legacy_badge_title": "Legacy custom parameters that could not be converted are ignored. Open the edit page for details.", + "status_waiting": "Waiting", + "status_running": "Running" + }, "user": { "action": "Action", "active": "Active", @@ -1290,7 +1411,6 @@ "clear_recent_successful_connections": "Clear seen successful connections", "client_configuration": "Show configuration guides for email clients and smartphones", "create_app_passwd": "Create app password", - "create_syncjob": "Create new sync job", "created_on": "Created on", "daily": "Daily", "day": "day", @@ -1400,16 +1520,6 @@ "spamfilter_wl_desc": "Allowlisted email addresses are programmed to never classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.", "spamfilter_yellow": "Yellow: this message may be spam, will be tagged as spam and moved to your junk folder", "status": "Status", - "sync_jobs": "Sync jobs", - "syncjob_check_log": "Check log", - "syncjob_last_run_result": "Last run result", - "syncjob_EX_OK": "Success", - "syncjob_EXIT_CONNECTION_FAILURE": "Connection problem", - "syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem", - "syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password", "tag_handling": "Set handling for tagged mail", "tag_help_example": "Example for a tagged email address: me+Facebook@example.org", "tag_help_explain": "In subfolder: a new subfolder named after the tag will be created below INBOX (\"INBOX/Facebook\").
\r\nIn subject: the tags name will be prepended to the mails subject, example: \"[Facebook] My News\".", diff --git a/data/web/lang/lang.es-es.json b/data/web/lang/lang.es-es.json index 1fb7becfd..9812a98c2 100644 --- a/data/web/lang/lang.es-es.json +++ b/data/web/lang/lang.es-es.json @@ -41,19 +41,12 @@ "alias_address_info": "Dirección(es) de correo completa(s) ó @dominio.com, para atrapar todos los mensajes para un dominio (separado por coma). Dominios que existan en mailcow solamente.", "alias_domain": "Dominio alias", "alias_domain_info": "Nombres de dominio válidos solamente (separado por coma).", - "automap": "Intentar enlazar carpetas (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Opciones del respaldo MX:", - "custom_params": "Parámetros personalizados", - "custom_params_hint": "Correcto: --param=xy, Incorrecto: --param xy", - "delete1": "Eliminar de la fuente cuando se complete", - "delete2": "Eliminar mensajes en el destino que no están en la fuente", - "delete2duplicates": "Eliminar duplicados en el destino", "description": "Descripción", "destination": "Destino", "domain": "Dominio", "domain_quota_m": "Cuota total del dominio (MiB)", "enc_method": "Método de cifrado", - "exclude": "Excluir objectos (regex)", "full_name": "Nombre completo", "gal": "Lista global de direcciones (GAL)", "gal_info": "El GAL contiene todos los objetos de un dominio y no puede ser editado por ningún usuario. Falta información de disponibilidad en SOGo, si está desactivada. Reinicia SOGo para aplicar los cambios.", @@ -68,7 +61,6 @@ "mailbox_username": "Nombre de usuario (parte izquierda de una dirección de correo)", "max_aliases": "Máx. alias posibles", "max_mailboxes": "Máx. buzones posibles", - "mins_interval": "Intervalo de sondeo (minutos)", "multiple_bookings": "Múltiples reservas", "nexthop": "Siguiente destino", "password": "Contraseña", @@ -83,15 +75,9 @@ "select_domain": "Elija primero un dominio", "sieve_desc": "Descripción", "sieve_type": "Tipo de filtro", - "skipcrossduplicates": "Omitir mensajes duplicados en carpetas (orden de llegada)", - "subscribeall": "Suscribirse a todas las carpetas", - "syncjob": "Añadir trabajo de sincronización", - "syncjob_hint": "Tenga en cuenta que las contraseñas deben guardarse en texto plano sin cifrar", "target_address": "Direcciones destino:", "target_address_info": "Dirección(es) de correo completa(s) (separado por coma).", "target_domain": "Dominio destino:", - "timeout1": "Tiempo de espera para la conexión al host remoto", - "timeout2": "Tiempo de espera para la conexión al host local", "username": "Usuario", "validate": "Validar", "validation_success": "Validado exitosamente", @@ -101,7 +87,6 @@ "public_comment": "Comentarios públicos", "disable_login": "Desactivar login (el correo entrante seguirá activo)", "comment_info": "Los comentarios privados no son visibles al usuario, mientras que los comentarios públicos aparecerán sobre la información general del usuario", - "dry": "Simular la sincronización", "private_comment": "Comentario privado", "app_passwd_protocols": "Protocolos autorizados para la contraseña de la aplicación", "relay_transport_info": "
Información
Puede definir mapas de transporte para un destino personalizado para este dominio. En caso de no definirlo, se realizará una búsqueda MX.", @@ -616,14 +601,10 @@ "edit": { "active": "Activo", "alias": "Editar alias", - "automap": "Intentar enlazar carpetas (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Opciones del respaldo MX:", "bcc_dest_format": "El destino BCC debe ser una única dirección de correo electrónico válida", "client_id": "ID de Cliente", "client_secret": "Secreto de cliente", - "delete1": "Eliminar de la fuente cuando se complete", - "delete2": "Eliminar mensajes en el destino que no están en la fuente", - "delete2duplicates": "Eliminar duplicados en el destino", "description": "Descripción:", "domain": "Editar dominio", "domain_admin": "Editar administrador del dominio", @@ -632,7 +613,6 @@ "dont_check_sender_acl": "No verificar remitente para el dominio %s", "edit_alias_domain": "Editar alias de dominio", "encryption": "Cifrado", - "exclude": "Excluir objectos (regex)", "force_pw_update": "Forzar cambio de contraseña en el próximo inicio de sesión", "force_pw_update_info": "Este usuario solo podrá iniciar sesión en la interfaz de usuario.", "full_name": "Nombre completo", @@ -648,9 +628,6 @@ "max_aliases": "Máx. alias:", "max_mailboxes": "Máx. buzones posibles:", "max_quota": "Máx. cuota por buzón (MiB):", - "maxage": "Antigüedad máxima de los mensajes en días que se migrarán
(0 = ignorar edad)", - "maxbytespersecond": "Max. bytes por segundo
(0 = ilimitado)", - "mins_interval": "Intervalo (min)", "multiple_bookings": "Múltiples reservas", "nexthop": "Siguiente destino", "password": "Contraseña:", @@ -669,13 +646,8 @@ "sender_acl": "Permitir envío como:", "sieve_desc": "Descripción", "sieve_type": "Tipo de filtro", - "skipcrossduplicates": "Omitir mensajes duplicados en carpetas (orden de llegada)", - "subfolder2": "Sincronizar en subcarpeta en destino
(vacío = no usar subcarpeta)", - "syncjob": "Editar trabajo de sincronización", "target_address": "Dirección/es destino (separadas por coma):", "target_domain": "Dominio destino:", - "timeout1": "Tiempo de espera para la conexión al host remoto", - "timeout2": "Tiempo de espera para la conexión al host local", "title": "Editar objeto", "unchanged_if_empty": "Si no hay cambios dejalo en blanco", "username": "Nombre de usuario", @@ -911,7 +883,6 @@ "set_prefilter": "Marcar como prefiltro", "sieve_info": "Puede almacenar varios filtros por usuario, pero solo un prefiltro y un postfiltro pueden estar activos al mismo tiempo.
\r\nCada filtro será procesado en el orden descrito. Ni un script fallido ni un \"keep;\" emitido detendrá el procesamiento de otros scripts.
\r\nPrefiltro sieve global → Prefiltro → Scripts del usuario → Posfiltro → Posfiltro sieve global", "status": "Estado", - "sync_jobs": "Trabajos de sincronización", "target_address": "Dirección destino", "target_domain": "Dominio destino", "tls_enforce_in": "Forzar TLS entrance", @@ -931,7 +902,6 @@ "sieve_preset_4": "Colocar en bandeja de entrada, omitir procesamiento posterior en filtros de sieve", "goto_spam": "Aprender como correo no deseado", "sieve_preset_header": "Vea los preajustes de ejemplo más abajo. Para más detalles, consulte Wikipedia.", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nombre de usuario o contraseña incorrectos", "tls_policy_maps_enforced_tls": "Estas políticas tendrán precedencia también para aquellos usuarios de buzones en los que sea obligatoria una conexión TLS. Si no se indica ninguna política a continuación, dichos usuarios aplicarán los valores predeterminados que se especifiquen en smtp_tls_mandatory_protocols y smtp_tls_mandatory_ciphers.", "alias_domain_alias_hint": "Los alias no se aplican a dominios de alias automáticamente. Una dirección alias mi-alias@dominio no cubre la dirección mi-alias@dominio-alias (donde \"dominio-alias\" es un hipotético alias para el dominio \"dominio\").
Utilice un filtro sieve para redirigir el correo a un buzón externo (ver la pestaña \"Filtros\" o utilice SOGo -> Desvío). Utilice \"expandir alias a dominio de alias\" para agregar automáticamente los alias que falten.", "domain_templates": "Plantillas de dominio", @@ -975,14 +945,6 @@ "sogo_visible": "Alias visible en SOGo", "sogo_visible_n": "Ocultar alias en SOGo", "stats": "Estadísticas", - "syncjob_check_log": "Comprobar registros", - "syncjob_last_run_result": "Resultado de la última ejecución", - "syncjob_EX_OK": "Éxito", - "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexión", - "syncjob_EXIT_TLS_FAILURE": "Problema con la conexión cifrada", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticación", - "syncjob_EXIT_OVERQUOTA": "El buzón de destino ha superado la cuota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "No es posible conectar con el servidor remoto", "table_size": "Tamaño de la tabla", "table_size_show_n": "Mostrar %s elementos", "templates": "Plantillas", @@ -1189,6 +1151,37 @@ "u2f_deprecated": "Parece que su llave se registró con el método obsoleto U2F. Desactivaremos la autenticación de dos factores y eliminaremos su llave.", "u2f_deprecated_important": "Registre su llave en el panel de administración con el nuevo método WebAuthn." }, + "syncjobs": { + "sync_jobs": "Trabajos de sincronización", + "syncjob_check_log": "Comprobar registros", + "syncjob_last_run_result": "Resultado de la última ejecución", + "syncjob_EX_OK": "Éxito", + "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexión", + "syncjob_EXIT_TLS_FAILURE": "Problema con la conexión cifrada", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticación", + "syncjob_EXIT_OVERQUOTA": "El buzón de destino ha superado la cuota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "No es posible conectar con el servidor remoto", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nombre de usuario o contraseña incorrectos", + "add": "Añadir trabajo de sincronización", + "add_hint": "Tenga en cuenta que las contraseñas deben guardarse en texto plano sin cifrar", + "edit": "Editar trabajo de sincronización", + "create": "Crear nuevo trabajo de sincronización", + "dry": "Simular la sincronización", + "subfolder2": "Sincronizar en subcarpeta en destino
(vacío = no usar subcarpeta)", + "mins_interval": "Intervalo de sondeo (minutos)", + "maxage": "Antigüedad máxima de los mensajes en días que se migrarán
(0 = ignorar edad)", + "exclude": "Excluir objectos (regex)", + "automap": "Intentar enlazar carpetas (\"Sent items\", \"Sent\" => \"Sent\" etc.)", + "delete1": "Eliminar de la fuente cuando se complete", + "delete2": "Eliminar mensajes en el destino que no están en la fuente", + "delete2duplicates": "Eliminar duplicados en el destino", + "skipcrossduplicates": "Omitir mensajes duplicados en carpetas (orden de llegada)", + "subscribeall": "Suscribirse a todas las carpetas", + "timeout1": "Tiempo de espera para la conexión al host remoto", + "timeout2": "Tiempo de espera para la conexión al host local", + "maxbytespersecond": "Max. bytes por segundo
(0 = ilimitado)", + "custom_params": "Parámetros personalizados" + }, "user": { "action": "Acción", "active": "Activo", @@ -1203,7 +1196,6 @@ "aliases_also_send_as": "También permitido enviar como", "aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)", "change_password": "Cambiar contraseña", - "create_syncjob": "Crear nuevo trabajo de sincronización", "created_on": "Creado", "daily": "Cada día", "day": "Día", @@ -1258,7 +1250,6 @@ "spamfilter_wl_desc": "Direcciones en la lista blanca nunca clasificarán como spam. Se puede usar comodines. Filtro solo aplica a los alias directos (alias con un solo buzón de destino) excluyendo los alias catch-all.", "spamfilter_yellow": "Amarillo: este mensaje puede ser spam, será etiquetado como spam y movido a su carpeta de correo no deseado", "status": "Estado", - "sync_jobs": "Trabajos de sincronización", "tag_handling": "Establecer manejo para el correo etiquetado", "tag_help_example": "Ejemplo de una dirección email etiquetada: mi+Facebook@ejemplo.org", "tag_help_explain": "En subcarpeta: una nueva subcarpeta llamada como la etiqueta será creada debajo de INBOX (\"INBOX/Facebook\").
\r\nEn asunto: los nombres de las etiquetas serán añadidos al asunto de los correos, ejemplo: \"[Facebook] Mis Noticias\".", diff --git a/data/web/lang/lang.fi-fi.json b/data/web/lang/lang.fi-fi.json index 5dd6a9b2d..af6c0e72f 100644 --- a/data/web/lang/lang.fi-fi.json +++ b/data/web/lang/lang.fi-fi.json @@ -32,21 +32,14 @@ "alias_address_info": "Koko sähköpostiosoite tai @ esimerkki.com, jotta kaikki verkkotunnuksen viestit saadaan (pilkuilla erotetut). mailcow vain verkkotunnukset.", "alias_domain": "Alias-verkkotunnus", "alias_domain_info": "Vain kelvolliset verkkotunnukset (pilkuin erotetut).", - "automap": "Yritä yhdistää kansioita automaattisesti(\"Lähetetyt kohteet\", \"Lähetti\" => \"Lähetti\" etc.)", "backup_mx_options": "Varmuuskopioi MX-asetukset", "comment_info": "Yksityinen kommentti ei ole näkyvissä käyttäjälle, kun taas julkinen kommentti näytetään työkaluvihjeenä siirtäessäsi sitä käyttäjän katsaukseen", - "custom_params": "Muokatut parametrit", - "custom_params_hint": "Oikea: --param=xy, väärä: --param xy", - "delete1": "Poista lähteestä, kun se on valmis", - "delete2": "Poista määränpään viestit, joita ei ole lähteessä", - "delete2duplicates": "Poista päällekkäisyydet määräpaikasta", "description": "Kuvaus", "destination": "Määränpää", "domain": "Verkkotunnus", "domain_matches_hostname": "Verkkotunnus %s vastaa isäntänimeä", "domain_quota_m": "Verkkotunnuksen kokonaiskiintiö (MiB)", "enc_method": "Salausmenetelmä", - "exclude": "Sulje pois objects (regex)", "full_name": "Koko nimi", "gal": "Yleinen osoite luettelo", "gal_info": "GAL ( kalenteri ) sisältää kaikki verkkotunnus alueen objektit, eikä yksikään käyttäjä voi muokata sitä. Vapaat ja varatut ajat-tiedot SOGosta puuttuu, jos se on poistettu käytöstä! Uudelleen käynnistä SOGo ja ota muutokset käyttöön.", @@ -61,7 +54,6 @@ "mailbox_username": "Käyttäjätunnus (sähköpostiosoitteen osa)", "max_aliases": "Max. mahdolliset aliakset", "max_mailboxes": "Max. mahdolliset sähkö postilaatikot", - "mins_interval": "Äänestysväli (minuutit)", "multiple_bookings": "Useita varauksia", "nexthop": "Seuraava pysäkki", "password": "Salasana", @@ -79,15 +71,9 @@ "select_domain": "Valitse ensin verkkotunnus", "sieve_desc": "Lyhyt kuvaus", "sieve_type": "Suodattimen tyyppi", - "skipcrossduplicates": "Ohita päällekkäiset viestit kansioiden välillä (ensimmäinen tulee, ensimmäinen palvella)", - "subscribeall": "Tilaa kaikki kansiot", - "syncjob": "Lisää synkronointityö", - "syncjob_hint": "Huomaa, että salasanat on tallennettava pelkkänä tekstinä!", "target_address": "Siirry osoitteisiin", "target_address_info": "Koko sähköpostiosoite (pilkuilla erotettu).", "target_domain": "Kohdetunnus", - "timeout1": "Yhteyden aikakatkaisu etäisäntään", - "timeout2": "Aikakatkaisu yhteyden muodostamiseen paikalliseen isäntään", "username": "Käyttäjätunnus", "validate": "Vahvista", "validation_success": "Vahvistettu onnistuneesti" @@ -405,15 +391,11 @@ "edit": { "active": "Aktiivinen", "alias": "Muokkaa aliasta", - "automap": "Yritä yhdistää kansioita automaattisesti(\"Lähetetyt kohteet\", \"Lähetti\" => \"Lähetti\" etc.)", "backup_mx_options": "Varmuus kopioinnin MX-asetukset", "bcc_dest_format": "BCC-piilo-kopio kohteen on oltava yksittäinen kelvollinen sähköposti osoite.", "client_id": "Asiakastunnus ID", "client_secret": "Asiakas salaisuus", "comment_info": "Yksityinen kommentti ei ole näkyvissä käyttäjälle, kun taas julkinen kommentti näytetään työkaluvihjeenä siirtäessäsi sitä käyttäjän katsaukseen", - "delete1": "Poista lähteestä, kun se on valmis", - "delete2": "Poista määränpään viestit, joita ei ole lähteessä", - "delete2duplicates": "Poista päällekkäisyydet määräpaikasta", "description": "Kuvaus", "domain": "Muokkaa verkkotunnus aluetta", "domain_admin": "Muokkaa verkkotunnus alueen järjestelmänvalvojaa", @@ -422,7 +404,6 @@ "dont_check_sender_acl": "Poista lähettäjän verkkotunnus alueen tarkistus käytöstä %s (+ alias-verkkotunnukset)", "edit_alias_domain": "Alias-verkkotunnus alueen muokkaaminen", "encryption": "Salaus", - "exclude": "Objektien jättäminen pois (regex)", "extended_sender_acl": "Ulkoisten lähettäjien osoitteet", "extended_sender_acl_info": "DKIM-verkkotunnuksen avain tulisi tuoda, jos se on käytettävissä.
\r\nMuista lisätä tämä palvelin vastaavaan SPF TXT-tietueeseen.
\r\nAina kun verkkotunnus tai aliaksen verkkotunnus lisätään tähän palvelimeen, joka on päällekkäinen ulkoisen osoitteen kanssa, ulkoinen osoite poistetaan.
\r\nKäytä @ domain.tld antaaksesi lähettää muodossa *@domain.tld.", "force_pw_update": "Pakota salasanan vaihto seuraavan sisään kirjautumisen jälkeen", @@ -440,10 +421,7 @@ "max_aliases": "Maks. Aliaksia", "max_mailboxes": "Maksimi. mahdolliset posti laatikot", "max_quota": "Maks. Kiintiö sähköposti laatikkoa kohden (MiB)", - "maxage": "Viestien enimmäisikä päivinä, jotka lähetetään kyselyllä etänä
(0 = ignore age)", - "maxbytespersecond": "Maksimi. tavua sekunnissa
(0 = rajoittamaton)", "mbox_rl_info": "Tätä nopeutta käytetään SASL-Kirjautumisnimessä, se vastaa mitä tahansa kirjautuneen käyttäjän käyttämän osoitteen \"from\"-osoitetta.Sähköpostin nopeus rajoitus ohittaa toimialuelaajuisen kappale rajoituksen.", - "mins_interval": "Aikaväli (min)", "multiple_bookings": "Useita varauksia", "nexthop": "Seuraava pysäkki", "password": "Salasana", @@ -466,18 +444,13 @@ "sender_acl_info": "Jos postilaatikon käyttäjän A sallitaan lähettävän postilaatikon käyttäjäksi B, lähettäjän osoitetta ei näytetä automaattisesti valittavana \"alkaen\" -kentässä SOGossa.
\r\nSähkö postilaatikon käyttäjän A on luotava valtuutus SOGoon, jotta sähkö postilaatikon käyttäjä b voi valita osoitteen lähettäjäksi. Tämä käyttäytyminen ei koske alias-osoitteita", "sieve_desc": "Lyhyt kuvaus", "sieve_type": "Suodattimen tyyppi", - "skipcrossduplicates": "Ohita päällekkäiset viestit kansioiden välillä (ensimmäinen tulee, ensimmäinen palvella)", "sogo_visible": "Alias näkyy SOGo-kohteessa", "sogo_visible_info": "Tämä asetus vaikuttaa vain objekteihin, jotka voidaan näyttää SOGo (jaetut tai ei-jaetut alias-osoitteet, jotka viittaavat vähintään yhteen paikalliseen posti laatikkoon).", "spam_alias": "Luo tai muuta aika rajoitettuja aliaksen osoitteita", "spam_policy": "Lisää tai Poista kohteita sallituista-/mustalta listalta", "spam_score": "Mukautetun roska posti arvon asettaminen", - "subfolder2": "Synkronoi kohteen alikansioon
(tyhjä = älä käytä alikansioon)", - "syncjob": "Muokkaa synkronointi työtä", "target_address": "Siirry osoitteeseen/es (pilkulla-erotettu)", "target_domain": "Kohde verkkotunnus alue", - "timeout1": "Yhteyden aikakatkaisu etäisäntään", - "timeout2": "Aikakatkaisu yhteyden muodostamiseen paikalliseen isäntään", "title": "Muokkaa objektia", "unchanged_if_empty": "Jos muuttumaton jätä tyhjäksi", "username": "Käyttäjätunnus", @@ -623,7 +596,6 @@ "sogo_visible_y": "Näytä alias SOGo-kohteessa", "spam_aliases": "Temp. alias", "status": "Tila", - "sync_jobs": "Synkronoi työt", "target_address": "Siiretty osoitteseen", "target_domain": "Kohde verkkotunnus alue", "tls_enforce_in": "Pakota TLS saapuva", @@ -786,6 +758,27 @@ "waiting_usb_register": "Odotetaan USB-laitetta...

Anna salasanasi yltä ja vahvista WebAuthn-rekisteröinti napauttamalla painiketta WebAuthn USB-laitteessa.", "yubi_otp": "Yubico OTP-todennus" }, + "syncjobs": { + "sync_jobs": "Synkronoi työt", + "add": "Lisää synkronointityö", + "add_hint": "Huomaa, että salasanat on tallennettava pelkkänä tekstinä!", + "edit": "Muokkaa synkronointi työtä", + "create": "Luo uusi synkronointi työ", + "subfolder2": "Synkronoi kohteen alikansioon
(tyhjä = älä käytä alikansioon)", + "mins_interval": "Äänestysväli (minuutit)", + "maxage": "Viestien enimmäisikä päivinä, jotka lähetetään kyselyllä etänä
(0 = ignore age)", + "exclude": "Sulje pois objects (regex)", + "automap": "Yritä yhdistää kansioita automaattisesti(\"Lähetetyt kohteet\", \"Lähetti\" => \"Lähetti\" etc.)", + "delete1": "Poista lähteestä, kun se on valmis", + "delete2": "Poista määränpään viestit, joita ei ole lähteessä", + "delete2duplicates": "Poista päällekkäisyydet määräpaikasta", + "skipcrossduplicates": "Ohita päällekkäiset viestit kansioiden välillä (ensimmäinen tulee, ensimmäinen palvella)", + "subscribeall": "Tilaa kaikki kansiot", + "timeout1": "Yhteyden aikakatkaisu etäisäntään", + "timeout2": "Aikakatkaisu yhteyden muodostamiseen paikalliseen isäntään", + "maxbytespersecond": "Maksimi. tavua sekunnissa
(0 = rajoittamaton)", + "custom_params": "Muokatut parametrit" + }, "user": { "action": "Toiminnot", "active": "Aktiivinen", @@ -802,7 +795,6 @@ "aliases_send_as_all": "Älä tarkista lähettäjän käyttö oikeuksia seuraaville verkkotunnus alueille ja sen alias-verkko tunnuksille", "change_password": "Vaihda salasana", "client_configuration": "Näytä sähköposti ohjelmien ja älypuhelimien konfigurointi oppaat", - "create_syncjob": "Luo uusi synkronointi työ", "daily": "Päivittäin", "day": "Päivä", "description": "Kuvaus", @@ -864,7 +856,6 @@ "spamfilter_wl_desc": "Sallittujen sähköposti osoitteita ei koskaan luokitella roska postiksi. Yleismerkkejä, voidaan käyttää. Suodatinta käytetään vain suora aliaksia (aliaksia, joilla on yksi kohde posti laatikko), lukuun ottamatta ( catch-all )saalistaa-kaikki aliaksia ja itse posti laatikkoa..", "spamfilter_yellow": "Keltainen: Tämä viesti voi olla roska postia, merkitään roska postiksi ja siirretään roska posti kansioon", "status": "Tila", - "sync_jobs": "Synkronoi työt", "tag_handling": "Merkittyjen Sähkö posti viestien käsittelyn määrittäminen", "tag_help_example": "Esimerkki merkitylle Sähkö posti osoitteeksi: me+Facebook@example.org", "tag_help_explain": "Alikansio: uusi tunnisteen jälkeen nimetty alikansio luodaan Saapuneet-kansion alapuolelle (\"Saapuneet/Facebook\").
\r\nAihe: tunnisteiden nimi lisätään sähkö postit-aiheeseen, esimerkiksi: \"[Facebook] My News\".", diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json index 9ee916713..5397db2d0 100644 --- a/data/web/lang/lang.fr-fr.json +++ b/data/web/lang/lang.fr-fr.json @@ -43,14 +43,8 @@ "alias_domain_info": "Seulement des noms de domaines valides (séparés par des virgules).", "app_name": "Nom de l'application", "app_password": "Mot de passe de l'application", - "automap": "Tenter de cibler automatiquement les dossiers (« Sent items », « Sent » => « Sent » etc.)", "backup_mx_options": "Options de MX secondaire", "comment_info": "Un commentaire privé n'est pas visible pour l'utilisateur, tandis qu'un commentaire public est affiché sous forme d'info-bulle lorsqu'on le survole dans un aperçu des utilisateurs", - "custom_params": "Paramètres personnalisés", - "custom_params_hint": "Correct : --param=xy, incorrect : --param xy", - "delete1": "Supprimer sur la source lorsque la synchronisation est terminée", - "delete2": "Supprimer les messages sur la destination qui ne sont pas présents sur la source", - "delete2duplicates": "Supprimer les doublons sur la destination", "description": "Description", "destination": "Destination", "disable_login": "Désactiver l'authentification (les courriels entrants resteront acceptés)", @@ -58,7 +52,6 @@ "domain_matches_hostname": "Le domaine %s correspond à la machine (hostname)", "domain_quota_m": "Quota total du domaine (Mio)", "enc_method": "Méthode de chiffrement", - "exclude": "Exclure des objets (expression régulière - regex)", "full_name": "Nom complet", "gal": "Carnet d'Adresses Global (GAL)", "gal_info": "La liste d'adresse globale (GAL) contient tous les objets d'un domaine et ne peut être modifié par aucun utilisateur. Si elles sont désactivées,les informations libres/occupées dans SOGo sont cachées, ! Redémarrez SOGo pour appliquer les changements.", @@ -74,7 +67,6 @@ "mailbox_username": "Identifiant (partie gauche d'une adresse de courriel)", "max_aliases": "Nombre maximal d'alias", "max_mailboxes": "Nombre maximal de boîtes de réception", - "mins_interval": "Période de relève (minutes)", "multiple_bookings": "Inscriptions multiples", "nexthop": "Suivant", "password": "Mot de passe", @@ -94,22 +86,15 @@ "select_domain": "Sélectionner d'abord un domaine", "sieve_desc": "Description courte", "sieve_type": "Type de filtre", - "skipcrossduplicates": "Ignorer les messages en double dans les dossiers (premier arrivé, premier servi)", - "subscribeall": "S'abonner à tous les dossiers", - "syncjob": "Ajouter une tâche de synchronisation", - "syncjob_hint": "Sachez que les mots de passe seront enregistrés en clair !", "target_address": "Aller à l'adresse", "target_address_info": "Adresse(s) de courriel complète(s) (séparées par des virgules).", "target_domain": "Domaine cible", - "timeout1": "Délai d'expiration pour la connexion à l'hôte distant", - "timeout2": "Délai d'expiration pour la connexion à l'hôte local", "username": "Nom d'utilisateur", "validate": "Valider", "validation_success": "Validation réussie", "bcc_dest_format": "La destination Cci doit être une seule adresse de courriel valide.
Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.", "tags": "Etiquettes", "app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application", - "dry": "Simuler la synchronisation", "internal": "Interne", "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.", "sender_allowed": "Autoriser l'envoi sous cet alias" @@ -619,15 +604,11 @@ "allowed_protocols": "Protocoles autorisés pour l'accès direct de l'utilisateur (n'affecte pas les protocoles de mot de passe de l'application)", "app_name": "Nom de l'application", "app_passwd": "Mot de passe de l'application", - "automap": "Tenter de cibler automatiquement les dossiers (« Sent items », « Sent » => « Sent » etc.)", "backup_mx_options": "Options Backup MX", "bcc_dest_format": "La destination BCC doit être une seule adresse de courriel valide.", "client_id": "ID client", "client_secret": "Secret client", "comment_info": "Un commentaire privé n’est pas visible pour l’utilisateur, tandis qu’un commentaire public est affiché comme infobulle lorsque vous le placez dans un aperçu des utilisateurs", - "delete1": "Supprimer de la source une fois terminé", - "delete2": "Supprimer les messages sur la destination qui ne sont pas sur la source", - "delete2duplicates": "Supprimer les doublons sur la destination", "delete_ays": "Veuillez confirmer le processus de suppression.", "description": "Description", "disable_login": "Refuser l’ouverture de session (le courrier entrant est toujours accepté)", @@ -638,7 +619,6 @@ "dont_check_sender_acl": "Désactiver la vérification de l’expéditeur pour le domaine %s (+ alias de domaines)", "edit_alias_domain": "Édition des alias de domaine", "encryption": "Chiffrement", - "exclude": "Exclure des objets (regex)", "extended_sender_acl": "Adresses de l’expéditeur externe", "extended_sender_acl_info": "Une clé de domaine DKIM doit être importée, si disponible.
\r\n N’oubliez pas d’ajouter ce serveur à l’enregistrement TXT SPF correspondant.
\r\n Chaque fois qu’un domaine ou un alias de domaine est ajouté à ce serveur, et qui chevauche une adresse externe, l’adresse externe est supprimée.
\r\n Utiliser @domain.tld pour permettre l'envoi comme *@domain.tld.", "force_pw_update": "Forcer la mise à jour du mot de passe à la prochaine ouverture de session", @@ -657,10 +637,7 @@ "max_aliases": "Nombre max. d'alias", "max_mailboxes": "Nombre max. de boîte de réception possibles", "max_quota": "Quota max. par boîte de réception (Mio)", - "maxage": "Âge maximal en jours des messages qui seront consultés à distance
(0 = ignorer la durée)", - "maxbytespersecond": "Octets max. par seconde
(0 = pas de limite)", "mbox_rl_info": "Cette limite d'envoi est appliquée au nom de connexion SASL, elle correspond à toute adresse « from » utilisée par l’utilisateur connecté. Une limite d'envoi pour les boîtes de réception remplace une limite d'envoi pour l’ensemble du domaine.", - "mins_interval": "Intervalle (min)", "multiple_bookings": "Réservations multiples", "nexthop": "Saut suivant", "password": "Mot de passe", @@ -695,19 +672,14 @@ "sender_acl_info": "Si l’utilisateur de la boîte de réception A est autorisé à envoyer en tant qu’utilisateur de la boîte de réception B, l’adresse de l’expéditeur n’est pas automatiquement affichée comme sélectionnable du champ « de » dans SOGo.
\n L’utilisateur B de la boîte de réception doit créer une délégation dans SOGo pour permettre à l’utilisateur A de la boîte de réception de sélectionner son adresse comme expéditeur. Pour déléguer une boîte de réception dans SOGo, utilisez le menu (trois points) à droite du nom de votre boîte dans le coin supérieur gauche dans la vue de courrier. Ce comportement ne s’applique pas aux adresses alias.", "sieve_desc": "Description courte", "sieve_type": "Type de filtre", - "skipcrossduplicates": "Ignorer les messages en double dans les dossiers (premier arrivé, premier servi)", "sogo_visible": "Alias visible dans SOGo", "sogo_visible_info": "Cette option affecte uniquement les objets qui peuvent être affichés dans SOGo (adresses alias partagées ou non partagées pointant vers au moins une boîte de réception locale). Si caché, un alias n’apparaîtra pas comme expéditeur sélectionnable dans SOGo.", "spam_alias": "Créer ou modifier des adresses alias limitées dans le temps", "spam_filter": "Filtre spam", "spam_policy": "Ajouter ou supprimer des éléments à la liste blanche/noire", "spam_score": "Définir un score spam personnalisé", - "subfolder2": "Synchronisation dans le sous-dossier sur la destination
(vide = ne pas utiliser de sous-dossier)", - "syncjob": "Modifier la tâche de synchronisation", "target_address": "Adresse(s) Goto (séparé(s) par des virgules)", "target_domain": "Domaine cible", - "timeout1": "Délai de connexion à l’hôte distant", - "timeout2": "Délai de connexion à l’hôte local", "title": "Éditer l'objet", "unchanged_if_empty": "Si non modifié, laisser en blanc", "username": "Nom d'utilisateur", @@ -946,7 +918,6 @@ "spam_aliases": "Alias temporaire", "stats": "Statistiques", "status": "Statut", - "sync_jobs": "Tâches de synchronisation", "table_size": "Taille de la table", "table_size_show_n": "Montrer %s articles", "target_address": "Goto adresse", @@ -976,20 +947,11 @@ "mailbox_templates": "Modèle de boîte de réception", "relay_unknown": "Relayer les boîtes de réception inconnues", "all_domains": "Tous les domaines", - "syncjob_EXIT_OVERQUOTA": "Quota dépassé de la boîte de réception cible", "domain_templates": "Modèles de domaine", - "syncjob_last_run_result": "Résultat de la dernière exécution", "sender": "Expéditeur", "add_template": "Ajouter un modèle", - "syncjob_EX_OK": "Succès", - "syncjob_EXIT_CONNECTION_FAILURE": "Problème de connexion", - "syncjob_EXIT_TLS_FAILURE": "Problème avec le chiffrement de la connexion", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problème d'authentification", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossible de se connecter au serveur distant", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Utilisateur ou mot de passe erroné", "templates": "Modèles", "template": "Modèle", - "syncjob_check_log": "Vérifier le journal", "recipient": "Destinataire", "open_logs": "Afficher les journaux", "iam": "Fournisseur d'identité", @@ -1216,6 +1178,37 @@ "fido2_validation_failed": "La validation a échoué", "set_fido2_touchid": "Enregistrer Touch ID sur Apple M1" }, + "syncjobs": { + "sync_jobs": "Tâches de synchronisation", + "syncjob_check_log": "Vérifier le journal", + "syncjob_last_run_result": "Résultat du dernier lancement", + "syncjob_EX_OK": "Succès", + "syncjob_EXIT_CONNECTION_FAILURE": "Problème de connexion", + "syncjob_EXIT_TLS_FAILURE": "Problème de connexion chiffrée", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problème d'authentification", + "syncjob_EXIT_OVERQUOTA": "La boîte de réception cible a dépassé le quota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossible de se connecter au serveur distant", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nom d'utilisateur ou mot de passe incorrect", + "add": "Ajouter une tâche de synchronisation", + "add_hint": "Sachez que les mots de passe seront enregistrés en clair !", + "edit": "Modifier la tâche de synchronisation", + "create": "Créer une tâche de synchronisation", + "dry": "Simuler la synchronisation", + "subfolder2": "Synchronisation dans le sous-dossier sur la destination
(vide = ne pas utiliser de sous-dossier)", + "mins_interval": "Période de relève (minutes)", + "maxage": "Âge maximal en jours des messages qui seront consultés à distance
(0 = ignorer la durée)", + "exclude": "Exclure des objets (expression régulière - regex)", + "automap": "Tenter de cibler automatiquement les dossiers (« Sent items », « Sent » => « Sent » etc.)", + "delete1": "Supprimer sur la source lorsque la synchronisation est terminée", + "delete2": "Supprimer les messages sur la destination qui ne sont pas présents sur la source", + "delete2duplicates": "Supprimer les doublons sur la destination", + "skipcrossduplicates": "Ignorer les messages en double dans les dossiers (premier arrivé, premier servi)", + "subscribeall": "S'abonner à tous les dossiers", + "timeout1": "Délai d'expiration pour la connexion à l'hôte distant", + "timeout2": "Délai d'expiration pour la connexion à l'hôte local", + "maxbytespersecond": "Octets max. par seconde
(0 = pas de limite)", + "custom_params": "Paramètres personnalisés" + }, "user": { "action": "Action", "active": "Actif", @@ -1240,7 +1233,6 @@ "change_password": "Changer le mot de passe", "client_configuration": "Afficher les guides de configuration pour les clients de messagerie et les smartphones", "create_app_passwd": "Créer un mot de passe application", - "create_syncjob": "Créer une tâche de synchronisation", "daily": "Quotidien", "day": "jour", "description": "Description", @@ -1325,7 +1317,6 @@ "spamfilter_wl_desc": "La liste blanche est programmée pour ne jamais classer comme spam les adresses de courriel qu'elle contient. Des caractères génériques peuvent être utilisés. Un filtre n’est appliqué qu’aux alias directs (alias avec une seule boîte de réception cible), à l’exclusion des alias attrape-tout et d’une boîte de réception.", "spamfilter_yellow": "Jaune : ce message est peut-être un spam, il sera étiqueté comme spam et déplacé vers votre dossier Pourriel", "status": "Statut", - "sync_jobs": "Tâches de synchronisation", "tag_handling": "Régler la manipulation du courrier étiqueté", "tag_help_example": "Exemple pour une adresse de courriel étiquetée : me+Facebook@example.org", "tag_help_explain": "Dans un sous-dossier : un nouveau sous-dossier nommé selon l'étiquette sera créé sous INBOX (« INBOX/Facebook »).
\nDans le sujet : le nom des balises sera ajouté au début du sujet de l'e-mail, exemple : « [Facebook] My News ».", @@ -1354,20 +1345,11 @@ "direct_protocol_access": "Cet utilisateur de la boîte aux lettres dispose d'un accès externe direct aux protocoles et applications suivants. Votre administrateur contrôle ce paramètre. Il est possible de créer des mots de passe d'application pour accorder l'accès à des protocoles et des applications individuels.
Le bouton « Connexion au Webmail » permet une connexion unique à SOGo et est toujours disponible.", "open_webmail_sso": "Connexion au Webmail", "recent_successful_connections": "Voir les connexions réussies", - "syncjob_EXIT_TLS_FAILURE": "Problème de connexion chiffrée", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problème d'authentification", - "syncjob_EXIT_OVERQUOTA": "La boîte de réception cible a dépassé le quota", "login_history": "Historique des connexions", "change_password_hint_app_passwords": "Votre compte a %d mots de passe d'application qui ne seront pas modifiés. Pour les gérer, allez dans l'onglet Mots de passe de l'application.", "clear_recent_successful_connections": "Vider l'historique des connexions réussies", "created_on": "Créé le", "last_ui_login": "Dernière connexion sur l'interface", - "syncjob_check_log": "Vérifier le journal", - "syncjob_last_run_result": "Résultat du dernier lancement", - "syncjob_EX_OK": "Succès", - "syncjob_EXIT_CONNECTION_FAILURE": "Problème de connexion", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossible de se connecter au serveur distant", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nom d'utilisateur ou mot de passe incorrect", "value": "Valeur", "allowed_protocols": "Protocoles autorisés", "mailbox": "Boîte de réception", diff --git a/data/web/lang/lang.gr-gr.json b/data/web/lang/lang.gr-gr.json index a070ea820..4b28dcdfb 100644 --- a/data/web/lang/lang.gr-gr.json +++ b/data/web/lang/lang.gr-gr.json @@ -1,4 +1,13 @@ { + "syncjobs": { + "dry": "Προσομοίωση συγχρονισμού", + "exclude": "Εξαίρεση αντικειμένων (regex)", + "automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)", + "delete1": "Διαγραφή όταν ολοκληρωθεί", + "delete2": "Διαγραφή μηνυμάτων στον προορισμό που δεν βρίσκονται στην πηγή", + "delete2duplicates": "Διαγραφή διπλότυπων στον προορισμό", + "custom_params": "Προσαρμοσμένες παράμετροι" + }, "user": { "verify": "Επαλήθευση", "week": "εβδομάδα", @@ -63,24 +72,16 @@ "app_name": "Όνομα εφαρμογής", "app_password": "Προσθήκη κωδικού εφαρμογής", "app_passwd_protocols": "Επιτρεπόμενα πρωτόκολλα για κωδικούς εφαρμογών", - "automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)", "backup_mx_options": "Επιλογές αναμετάδοσης", "bcc_dest_format": "Η BCC διεύθυνση πρέπει να είναι μία και έγκυρη διεύθυνση e-mail.
Αν θέλετε να στείλετε αντίγραφα σε πολλούς παραλήπτες, δημιουργήστε ένα ψευδόνυμο για όλους και χρησιμοποιήστε το εδώ.", "comment_info": "Τα προσωπικά σχόλια δεν είναι ορατά στον χρήστη. Τα δημόσια σχόλια εμφανίζονται ως tooltips.", - "custom_params": "Προσαρμοσμένες παράμετροι", - "custom_params_hint": "Σωστή σύνταξη: --param=xy, λάθος σύνταξη: --param xy", - "delete1": "Διαγραφή όταν ολοκληρωθεί", - "delete2": "Διαγραφή μηνυμάτων στον προορισμό που δεν βρίσκονται στην πηγή", - "delete2duplicates": "Διαγραφή διπλότυπων στον προορισμό", "description": "Περιγραφή", "destination": "Προορισμός", "disable_login": "Απαγόρευση εισόδου (η εισερχόμενη αλληλογραφία εξακολουθεί να γίνεται δεκτή)", "domain": "Τομέας", "domain_matches_hostname": "Ο τομέας %s είναι ο ίδιος με το όνομα του διακομιστή", "domain_quota_m": "Συνολικό όριο τομέα (MiB)", - "dry": "Προσομοίωση συγχρονισμού", "enc_method": "Μέθοδος κρυπτογράφησης", - "exclude": "Εξαίρεση αντικειμένων (regex)", "full_name": "Πλήρες όνομα", "gal": "Κοινόχρηστη λίστα διευθύνσεων" }, @@ -180,7 +181,6 @@ "app_name": "Όνομα εφαρμογής", "app_passwd": "Κωδικός πρόσβασης εφαρμογής", "app_passwd_protocols": "Επιτρέπομενα πρωτόκολλα για τον κωδικό εφαρμογής", - "automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)", "backup_mx_options": "Επιλογές αναμετάδοσης" } } diff --git a/data/web/lang/lang.hu-hu.json b/data/web/lang/lang.hu-hu.json index ff49bc5ee..013df0313 100644 --- a/data/web/lang/lang.hu-hu.json +++ b/data/web/lang/lang.hu-hu.json @@ -318,14 +318,10 @@ "acl": "ACL (Engedélyek)", "admin": "Adminisztrátor szerkesztése", "app_passwd_protocols": "Engedélyezett protokollok az alkalmazás jelszavához", - "automap": "Próbálja automatikusan feltérképezni a mappákat (\"Elküldött elemek\", \"Elküldött\" => \"Elküldött\" stb.)", "bcc_dest_format": "A BCC-célpontnak egyetlen érvényes e-mail címnek kell lennie.
Ha több címre kell másolatot küldenie, hozzon létre egy aliast, és használja azt itt.", "comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.", "created_on": "Létrehozva", "custom_attributes": "Egyéni attribútumok", - "delete1": "Törlés a forrásból, ha befejeződött", - "delete2": "Üzenetek törlése a célállomáson, amelyek nincsenek a forráson", - "delete2duplicates": "Duplikáltak törlése a célállomáson", "delete_ays": "Erősítse meg a törlési folyamatot.", "disable_login": "Bejelentkezés letiltása (a bejövő leveleket továbbra is elfogadja)", "domain": "Domain szerkesztése", @@ -345,7 +341,6 @@ "domain_footer_skip_replies": "Lábléc figyelmen kívül hagyása válasz e-maileknél", "dont_check_sender_acl": "Küldő ellenőrzés letiltása a(z) %s tartományhoz (+ alias tartományok)", "encryption": "Titkosítás", - "exclude": "Objektumok kizárása (regex)", "extended_sender_acl": "Külső feladói címek", "extended_sender_acl_info": "Importálni kell egy DKIM tartomány kulcsot, ha elérhető.
\r\n Ne felejtse el hozzáadni ezt a szervert a megfelelő SPF TXT rekordhoz.
\r\n Amikor egy tartományt vagy alias tartományt ad hozzá ehhez a szerverhez, amely átfedésben van egy külső címmel, a külső cím eltávolításra kerül.
\r\n Használja a @domain.tld címet, hogy engedélyezze a küldést *@domain.tld néven.", "force_pw_update": "Jelszófrissítés kényszerítése a következő bejelentkezéskor", @@ -372,10 +367,7 @@ "max_aliases": "Max. aliasok", "max_mailboxes": "Max. lehetséges postafiókok", "max_quota": "Max. kvóta postafiókonként (MiB)", - "maxage": "Üzenetek maximális életkora napokban, amelyek lekérdezésre kerülnek a távolról
(0 = figyelmen kívül hagyás)", - "maxbytespersecond": "Max. bájt/másodperc
(0 = korlátlan)", "mbox_rl_info": "Ez a sebességkorlátozás a SASL bejelentkezési névre vonatkozik, és illeszkedik minden, a bejelentkezett felhasználó által használt \"from\" címre. Egy postafiók sebességkorlátozása felülírja a tartományszintű sebességkorlátozást.", - "mins_interval": "Időköz (perc)", "multiple_bookings": "Több foglalás", "none_inherit": "Nincs / Örökölt", "nexthop": "Következő ugrás", @@ -416,7 +408,6 @@ "sender_acl_info": "Ha az A postafiók felhasználója küldhet a B postafiók felhasználójaként, a feladói cím nem jelenik meg automatikusan választható \"from\" mezőként a SOGo-ban.
\r\n A B postafiók felhasználójának delegálást kell létrehoznia a SOGo-ban, hogy az A postafiók felhasználója kiválaszthassa a címét feladóként. A SOGo-ban egy postafiók delegálásához használja a menüt (három pont) a postafiók neve jobb oldalán a bal felső sarokban, a levélnézetben. Ez a viselkedés nem vonatkozik az alias címekre.", "sieve_desc": "Rövid leírás", "sieve_type": "Szűrő típusa", - "skipcrossduplicates": "Átugrani a duplikált üzeneteket a mappák között (aki előbb jön, előbb kapja)", "sogo_access": "Közvetlen továbbítás a SOGo-ra", "sogo_access_info": "A bejelentkezés után a felhasználó automatikusan átirányításra kerül a SOGo-ra.", "sogo_visible": "Alias látható a SOGo-ban", @@ -425,12 +416,8 @@ "spam_filter": "Spam szűrő", "spam_policy": "Elemek hozzáadása vagy eltávolítása a tiltó- vagy engedélyezési listán", "spam_score": "Egyéni spam pontszám beállítása", - "subfolder2": "Szinkronizálás a célállomáson egy almappába
(üres = ne használjon almappát)", - "syncjob": "Szinkronizálási feladat szerkesztése", "target_address": "Célcím(ek) (vesszővel elválasztva)", "target_domain": "Cél domain", - "timeout1": "Időtúllépés a távoli állomáshoz való csatlakozáskor", - "timeout2": "Időtúllépés a helyi állomáshoz való csatlakozáskor", "title": "Objektum szerkesztése", "unchanged_if_empty": "Ha nem változik, hagyja üresen", "username": "Felhasználónév", @@ -570,7 +557,6 @@ "spam_aliases": "Ideiglenes alias", "stats": "Statisztika", "status": "Státusz", - "sync_jobs": "Szinkronizáló feladatok", "table_size": "Tábla mérete", "table_size_show_n": "%s sor mutatása", "target_address": "Címhez ugrás", @@ -648,15 +634,6 @@ "sogo_visible": "Alias látható a SOGo-ban", "sogo_visible_n": "Alias elrejtése a SOGo-ban", "sogo_visible_y": "Alias megjelenítése a SOGo-ban", - "syncjob_check_log": "Napló ellenőrzése", - "syncjob_last_run_result": "Utolsó futás eredménye", - "syncjob_EX_OK": "Siker", - "syncjob_EXIT_CONNECTION_FAILURE": "Kapcsolati probléma", - "syncjob_EXIT_TLS_FAILURE": "Probléma a titkosított kapcsolattal", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Hitelesítési probléma", - "syncjob_EXIT_OVERQUOTA": "A cél postafiók túllépte a kvótát", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nem lehet csatlakozni a távoli szerverhez", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Hibás felhasználónév vagy jelszó", "templates": "Sablonok", "template": "Sablon", "tls_map_policy": "Irányelv", @@ -839,6 +816,37 @@ "verified_yotp_login": "Yubico OTP bejelentkezés ellenőrizve", "mailbox_renamed": "A postafiók átnevezve %s-ről %s-re" }, + "syncjobs": { + "sync_jobs": "Szinkronizáló feladatok", + "syncjob_check_log": "Napló ellenőrzése", + "syncjob_last_run_result": "Utolsó futás eredménye", + "syncjob_EX_OK": "Siker", + "syncjob_EXIT_CONNECTION_FAILURE": "Kapcsolati probléma", + "syncjob_EXIT_TLS_FAILURE": "Probléma a titkosított kapcsolattal", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Hitelesítési probléma", + "syncjob_EXIT_OVERQUOTA": "A cél postafiók túllépte a kvótát", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nem lehet csatlakozni a távoli szerverhez", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Hibás felhasználónév vagy jelszó", + "add": "Szinkronizálási feladat hozzáadása", + "add_hint": "Ne feledje, hogy a jelszavakat egyszerű szöveges formában kell elmenteni!", + "edit": "Szinkronizálási feladat szerkesztése", + "create": "Új szinkronizációs művelet létrehozása", + "dry": "Szinkronizálás szimulálása", + "subfolder2": "Szinkronizálás a célállomáson egy almappába
(üres = ne használjon almappát)", + "mins_interval": "A lekérdezési időköz (perc)", + "maxage": "Üzenetek maximális életkora napokban, amelyek lekérdezésre kerülnek a távolról
(0 = figyelmen kívül hagyás)", + "exclude": "Objektumok kizárása (regex)", + "automap": "Próbálja automatikusan feltérképezni a mappákat (\"Elküldött elemek\", \"Elküldött\" => \"Elküldött\" stb.)", + "delete1": "Törlés a forrásból, ha befejeződött", + "delete2": "A forráson kívüli üzenetek törlése a célállomáson", + "delete2duplicates": "Duplikáltak törlése a célállomáson", + "skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)", + "subscribeall": "Feliratkozás minden mappára", + "timeout1": "Időtúllépés a távoli állomáshoz való csatlakozáskor", + "timeout2": "Időtúllépés a helyi állomáshoz való csatlakozáskor", + "maxbytespersecond": "Max. bájt/másodperc
(0 = korlátlan)", + "custom_params": "Egyéni paraméterek" + }, "user": { "action": "Művelet", "active": "Aktív", @@ -858,7 +866,6 @@ "change_password": "Jelszó cseréje", "client_configuration": "Beállítások megjelenítése email-kliensekhez", "create_app_passwd": "Applikáció jelszó létrehozása", - "create_syncjob": "Új szinkronizációs művelet létrehozása", "daily": "Napi", "day": "nap", "description": "Leírás", @@ -920,7 +927,6 @@ "spamfilter_wl_desc": "Az engedélyezőlistán szereplő email címek soha nem kerülnek spam-be. Joker karakter használható. A szűrő csak közvetlen aliasokra vonatkozik (alias egyetlen cél-postafiókkal), magára a postafiókra, illetve mindent elkapó aliasokra nem.", "spamfilter_yellow": "Sárga: ez az üzenet spam-gyanús, spam-nek lesz címezve és elérhető lesz a levélszemét mappában", "status": "Státusz", - "sync_jobs": "Szinkronizáló feladatok", "tag_handling": "Címkézett üzenetek kezelése", "tag_help_example": "Példa címkézett email címre: en+Facebook@pelda.hu", "tag_help_explain": "Almappában: új almappa készül a címke nevét viselve (pl.: \"Bejövő/Facebook\").
\r\nTárgyban: a címke a tárgy elé lesz fűzve, pl.: \"[Facebook] Hírek\".", @@ -992,15 +998,6 @@ "sogo_profile_reset_help": "Ez tönkreteszi a felhasználó SOGo profilját, és visszavonhatatlanul törli az összes névjegy- és naptáradatot.", "sogo_profile_reset_now": "Profil visszaállítása most", "spamfilter_table_domain_policy": "n/a (tartományi szabályzat)", - "syncjob_check_log": "Napló ellenőrzése", - "syncjob_last_run_result": "Utolsó futás eredménye", - "syncjob_EX_OK": "Siker", - "syncjob_EXIT_CONNECTION_FAILURE": "Kapcsolati probléma", - "syncjob_EXIT_TLS_FAILURE": "Probléma a titkosított kapcsolattal", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Hitelesítési probléma", - "syncjob_EXIT_OVERQUOTA": "A cél postafiók túllépte a kvótát", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nem lehet csatlakozni a távoli szerverhez", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Hibás felhasználónév vagy jelszó", "tfa_info": "A kétlépcsős hitelesítés segít megvédeni a fiókodat. Ha engedélyezed, alkalmazás jelszavakra lesz szükséged, hogy bejelentkezz olyan alkalmazásokba vagy szolgáltatásokba, amelyek nem támogatják a kétlépcsős hitelesítést (pl. levelező kliensek).", "tls_policy_warning": "Figyelem: Ha titkosított levélátvitelt kényszerítesz ki, elveszíthetsz e-maileket.
Azok az üzenetek, amelyek nem felelnek meg a szabályzatnak, kemény hibával visszapattannak a levelezőrendszerből.
Ez az opció a fő e-mail címedre (bejelentkezési név), az alias tartományokból származó összes címre, valamint az alias címekre vonatkozik, amelyeknek csak ez az egy postafiók a célja.", "value": "Érték", @@ -1070,20 +1067,16 @@ "mailbox_quota_def": "Alapértelmezett postafiók kvóta", "password_repeat": "Megerősítő jelszó (ismétlés)", "post_domain_add": "A \"sogo-mailcow\" SOGo konténert újra kell indítani egy új tartomány hozzáadása után!

Kiegészítésképpen a tartományok DNS-konfigurációját is felül kell vizsgálni. A DNS-konfiguráció jóváhagyása után indítsa újra az \"acme-mailcow\"-t, hogy automatikusan generáljon tanúsítványokat az új tartományhoz (autoconfig.<domain>, autodiscover.<domain>).
Ez a lépés opcionális, és 24 óránként megismétlődik.", - "dry": "Szinkronizálás szimulálása", "inactive": "Inaktív", "kind": "Típus", "mailbox_quota_m": "Maximális kvóta postafiókonként (MiB)", "mailbox_username": "Felhasználónév (az e-mail cím bal oldali része)", "max_aliases": "Max. lehetséges álnevek", "max_mailboxes": "Max. lehetséges postafiókok", - "mins_interval": "A lekérdezési időköz (perc)", "password": "Jelszó", "port": "Port", "public_comment": "Nyilvános megjegyzés", "target_domain": "Céltartomány", - "timeout1": "Időtúllépés a távoli állomáshoz való csatlakozáskor", - "timeout2": "Időtúllépés a helyi állomáshoz való csatlakozáskor", "validate": "Érvényesítsd", "description": "Leírás", "destination": "Célállomás", @@ -1091,25 +1084,18 @@ "domain_matches_hostname": "Domain %s megegyezik a hostnévvel", "domain_quota_m": "Teljes tartományi kvóta (MiB)", "enc_method": "Titkosítási módszer", - "exclude": "Objektumok kizárása (regex)", "full_name": "Teljes név", "gal": "Globális címlista", "goto_ham": "Tanítás valódi levélként", "goto_null": "Leveleket csendben eldobni", "goto_spam": "Tanítás spamként", - "syncjob_hint": "Ne feledje, hogy a jelszavakat egyszerű szöveges formában kell elmenteni!", "target_address": "Továbbítási címek", "target_address_info": "Teljes e-mail cím(ek) (vesszővel elválasztva).", "bcc_dest_format": "A BCC-célpontnak egyetlen érvényes e-mail címnek kell lennie.
Ha több címre kell másolatot küldenie, hozzon létre egy aliast, és használja azt.", "comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.", - "custom_params": "Egyéni paraméterek", "gal_info": "A GAL tartalmazza a tartomány összes objektumát, és egyetlen felhasználó sem szerkesztheti. A SOGo-ban a Szabad/Elfoglalt információ hiányzik, ha ki van kapcsolva! Indítsa újra a SOGo-t a változások alkalmazásához.", "hostname": "Hoszt", "backup_mx_options": "Továbbítási opciók", - "custom_params_hint": "Megfelelő: --param=xy, Rossz: --param xy", - "delete1": "Törlés a forrásból, ha befejeződött", - "delete2": "A forráson kívüli üzenetek törlése a célállomáson", - "delete2duplicates": "Duplikáltak törlése a célállomáson", "domain": "Domain", "nexthop": "Következő hop", "tags": "Címkék", @@ -1127,7 +1113,6 @@ "alias_domain_info": "Csak érvényes tartománynevek (vesszővel elválasztva).", "app_name": "Alkalmazás neve", "app_passwd_protocols": "Engedélyezett protokollok az alkalmazás jelszavához", - "automap": "Próbálja automatikusan feltérképezni a mappákat (\"Elküldött elemek\", \"Elküldött\" => \"Elküldött\" stb.)", "multiple_bookings": "Több foglalás", "quota_mb": "Kvóta (MiB)", "relay_all": "Az összes címzett továbbítása", @@ -1140,9 +1125,6 @@ "select_domain": "Kérjük, először válasszon egy domaint", "sieve_desc": "Rövid leírás", "sieve_type": "Szűrő típusa", - "skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)", - "subscribeall": "Feliratkozás minden mappára", - "syncjob": "Szinkronizálási feladat hozzáadása", "internal": "Belső", "internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető.", "sender_allowed": "Küldés engedélyezése ezzel az aliasszal" diff --git a/data/web/lang/lang.it-it.json b/data/web/lang/lang.it-it.json index de228b2d7..124a51eeb 100644 --- a/data/web/lang/lang.it-it.json +++ b/data/web/lang/lang.it-it.json @@ -43,14 +43,8 @@ "alias_domain_info": "Solo nomi di dominio validi (separati da virgola).", "app_name": "Nome app", "app_password": "Aggiungi la password dell'app", - "automap": "Prova a mappare automaticamente le cartelle (\"Sent items\", \"Sent\" => \"Posta inviata\" ecc.)", "backup_mx_options": "Opzioni di inoltro", "comment_info": "Un commento privato non è visibile all'utente, mentre un commento pubblico viene mostrato come suggerimento quando si passa con il mouse nella panoramica di un utente", - "custom_params": "Parametri personalizzati", - "custom_params_hint": "Corretto: --param=xy, errato: --param xy", - "delete1": "Elimina dalla sorgente al termine", - "delete2": "Elimina i messaggi nella casella di destinazione che non sono presenti nell'origine", - "delete2duplicates": "Elimina duplicati nella destinazione", "description": "Descrizione", "destination": "Destinazione", "disable_login": "Disabilita l'accesso (la posta in arrivo viene correttamente recapitata)", @@ -58,7 +52,6 @@ "domain_matches_hostname": "Il dominio %s corrisponde al'hostname", "domain_quota_m": "Spazio totale dominio (MiB)", "enc_method": "Metodo di crittografia", - "exclude": "Escludi oggetti (regex)", "full_name": "Nome completo", "gal": "Global Address List", "gal_info": "La GAL contiene tutti gli indirizzi del dominio e non può essere modificata da nessun utente. Le infomazioni sulla disponibilità di ogni utente non sono presenti in SOGo, se sono state disabilitate! Riavvia SOGo per applicare le modifiche.", @@ -74,7 +67,6 @@ "mailbox_username": "Username (parte a sinistra della @)", "max_aliases": "Numero massimo alias", "max_mailboxes": "Numero massimo caselle di posta", - "mins_interval": "intervallo (minuti)", "multiple_bookings": "Prenotazioni multiple", "nexthop": "Hop successivo", "password": "Password", @@ -94,15 +86,9 @@ "select_domain": "Seleziona prima un dominio", "sieve_desc": "Descrizione breve", "sieve_type": "Tipologia di filtro", - "skipcrossduplicates": "Salta i messaggi duplicati tra le cartelle (precedenza al primo arrivato)", - "subscribeall": "Iscriviti a tutte le cartelle", - "syncjob": "Aggiungi sincronizzazione", - "syncjob_hint": "Ricordati che le password vanno salvate in testo semplice!", "target_address": "Inoltra all'indirizzo", "target_address_info": "Indirizzo e-mail completo/i (separati da virgole).", "target_domain": "Dominio di destinazione", - "timeout1": "Timeout per la connessione all'host remoto", - "timeout2": "Timeout per la connessione all'host locale", "username": "Nome utente", "validate": "Convalida", "validation_success": "Convalidato con successo", @@ -575,16 +561,12 @@ "allowed_protocols": "Protocolli consentiti", "app_name": "App name", "app_passwd": "App password", - "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Backup MX options", "bcc_dest_format": "BCC destination must be a single valid email address.", "client_id": "Client ID", "client_secret": "Client secret", "created_on": "Creato il", "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview", - "delete1": "Elimina dalla sorgente al termine", - "delete2": "Delete messages on destination that are not on source", - "delete2duplicates": "Elimina duplicati nella destinazione", "delete_ays": "Si prega di confermare il processo di eliminazione.", "description": "Descrizione", "disable_login": "Disabilita l'accesso (la posta in arrivo viene correttamente recapitata)", @@ -595,7 +577,6 @@ "dont_check_sender_acl": "Disattiva il controllo del mittente per il dominio %s + alias di dominio", "edit_alias_domain": "Modifica alias di dominio", "encryption": "Crittografia", - "exclude": "Escludi oggetti (regex)", "extended_sender_acl": "External sender addresses", "extended_sender_acl_info": "A DKIM domain key should be imported, if available.
\r\n Remember to add this server to the corresponding SPF TXT record.
\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.
\r\n Use @domain.tld to allow to send as *@domain.tld.", "force_pw_update": "Forza l'aggiornamento della password al prossimo accesso", @@ -615,10 +596,7 @@ "max_aliases": "Numero massimo alias", "max_mailboxes": "Caselle di posta massime", "max_quota": "Massimo spazio per casella (MiB)", - "maxage": "Massima età dei messaggi che verranno scaricati dal server remoto
(0 = ignora età)", - "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", "mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.", - "mins_interval": "Intervallo (min)", "multiple_bookings": "Prenotazioni multiple", "nexthop": "Prossimo hop", "password": "Password", @@ -655,19 +633,14 @@ "sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.
\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.", "sieve_desc": "Breve descrizione", "sieve_type": "Filter type", - "skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)", "sogo_visible": "L'alias è visibile in SOGo", "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.", "spam_alias": "Create or change time limited alias addresses", "spam_filter": "Spam filter", "spam_policy": "Aggiungi o rimuovi elementi dalla whitelist/blacklist", "spam_score": "Imposta un punteggio spam personalizzato", - "subfolder2": "Sincronizza in una sottocartella
(vuoto = non sincronizzare in sottocartella)", - "syncjob": "Modifica sincronizzazione", "target_address": "Vai all'indirizzo/i (separato da virgola)", "target_domain": "Target dominio", - "timeout1": "Timeout per la connessione all'host remoto", - "timeout2": "Timeout per la connessione all'host remoto", "title": "Modifica oggetto", "unchanged_if_empty": "Se immutato lasciare vuoto", "username": "Nome utente", @@ -885,7 +858,6 @@ "spam_aliases": "Alias temporanei", "stats": "Statistiche", "status": "Stato", - "sync_jobs": "Processi di sync", "table_size": "Dimensioni della tabella", "table_size_show_n": "Mostra %s elementi", "target_address": "Vai agli indirizzi", @@ -906,22 +878,13 @@ "waiting": "In attesa", "weekly": "Settimanale", "yes": "✓", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati", "goto_ham": "Apprendi come ham", "goto_spam": "Apprendi come spam", "open_logs": "Apri i log", - "syncjob_check_log": "Controlla il log", - "syncjob_last_run_result": "Risultato dell'ultima esecuzione", - "syncjob_EXIT_TLS_FAILURE": "Problema con la connessione criptata", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema di autenticazione", - "syncjob_EXIT_OVERQUOTA": "La casella di posta del destinatario ha superato la quota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossibile connettersi al server remoto", - "syncjob_EXIT_CONNECTION_FAILURE": "Problema di connessione", "catch_all": "Catch-All", "sender": "Mittente", "all_domains": "Tutti i domini", "recipient": "Destinatario", - "syncjob_EX_OK": "Successo", "add_template": "Aggiungi template", "force_pw_update": "Forza il cambio della password al prossimo accesso", "relay_unknown": "Inoltra a caselle di posta sconosciute", @@ -1129,6 +1092,37 @@ "u2f_deprecated_important": "Registra la tua chiave nel pannello di amministrazione con il nuovo metodo WebAuthn.", "authenticators": "Autenticatori" }, + "syncjobs": { + "sync_jobs": "Processi di sync", + "syncjob_check_log": "Controlla i log", + "syncjob_last_run_result": "Risultato dell'ultima esecuzione", + "syncjob_EX_OK": "Successo", + "syncjob_EXIT_CONNECTION_FAILURE": "Problema di connessione", + "syncjob_EXIT_TLS_FAILURE": "Problema con la connessione criptata", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema di autenticazione", + "syncjob_EXIT_OVERQUOTA": "La casella di posta del destinatario ha superato la quota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossibile connettersi al server remoto", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati", + "add": "Aggiungi sincronizzazione", + "add_hint": "Ricordati che le password vanno salvate in testo semplice!", + "edit": "Modifica sincronizzazione", + "create": "Crea un azione di sync", + "dry": "Simula sincronizzazione", + "subfolder2": "Sincronizza in una sottocartella
(vuoto = non sincronizzare in sottocartella)", + "mins_interval": "intervallo (minuti)", + "maxage": "Massima età dei messaggi che verranno scaricati dal server remoto
(0 = ignora età)", + "exclude": "Escludi oggetti (regex)", + "automap": "Prova a mappare automaticamente le cartelle (\"Sent items\", \"Sent\" => \"Posta inviata\" ecc.)", + "delete1": "Elimina dalla sorgente al termine", + "delete2": "Elimina i messaggi nella casella di destinazione che non sono presenti nell'origine", + "delete2duplicates": "Elimina duplicati nella destinazione", + "skipcrossduplicates": "Salta i messaggi duplicati tra le cartelle (precedenza al primo arrivato)", + "subscribeall": "Iscriviti a tutte le cartelle", + "timeout1": "Timeout per la connessione all'host remoto", + "timeout2": "Timeout per la connessione all'host locale", + "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", + "custom_params": "Parametri personalizzati" + }, "user": { "action": "Azione", "active": "Attiva", @@ -1154,7 +1148,6 @@ "clear_recent_successful_connections": "Clear seen successful connections", "client_configuration": "Show configuration guides for email clients and smartphones", "create_app_passwd": "Create app password", - "create_syncjob": "Crea un azione di sync", "created_on": "Creato il", "daily": "Giornaliero", "day": "giorno", @@ -1253,7 +1246,6 @@ "spamfilter_wl_desc": "Email inserita nella whitelist per non essere mai riconosciuta come spam. Si possono usare le wildcards. Un filtro viene applicato solo agli alias diretti (alias con una singola casella postale di destinazione) esclusi gli alias catch-all e la casella postale stessa.", "spamfilter_yellow": "Giallo: Questo messaggio può essere spam, verrà segnalato come spam e spostato nella cartella spazzatura", "status": "Stato", - "sync_jobs": "Processi di sync", "tag_handling": "Imposta le gestione della mail evidenziate", "tag_help_example": "Esempio di mail con tag: ich+Facebook@example.org", "tag_help_explain": "In sottocartelle: Una nuova cartella con il nome del tag verrà creata sotto INBOX (\"INBOX/Facebook\").
\r\nNell'oggetto: Il nome del tag verrà aggiunto all'inizio dell'oggetto della mail, esempio: \"[Facebook] Le mie notizie\".", @@ -1276,18 +1268,9 @@ "year": "anno", "years": "anni", "change_password_hint_app_passwords": "Il tuo account ha %d password delle app che non verranno modificate. Per gestirle, vai alla scheda App passwords.", - "syncjob_check_log": "Controlla i log", - "syncjob_last_run_result": "Risultato dell'ultima esecuzione", "open_logs": "Apri i log", "apple_connection_profile_with_app_password": "Una nuova password dell'app è stata generata ed aggiunta al profilo così che non sia più necessario inserire alcuna password quando si imposta il dispositivo. Si prega di non condividere il file in quanto concede l'accesso completo alla tua casella di posta elettronica.", "allowed_protocols": "Protocolli consentiti", - "syncjob_EX_OK": "Successo", - "syncjob_EXIT_CONNECTION_FAILURE": "Problema di connessione", - "syncjob_EXIT_TLS_FAILURE": "Problema con la connessione criptata", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema di autenticazione", - "syncjob_EXIT_OVERQUOTA": "La casella di posta del destinatario ha superato la quota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossibile connettersi al server remoto", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati", "with_app_password": "con password dell'app", "direct_protocol_access": "Questo utente della mailbox ha accesso diretto ed esterno ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.
Il pulsante \"Accedi alla webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile.", "pushover_sound": "Suono", diff --git a/data/web/lang/lang.ja-jp.json b/data/web/lang/lang.ja-jp.json index 6dfa5d87b..cf067a5af 100644 --- a/data/web/lang/lang.ja-jp.json +++ b/data/web/lang/lang.ja-jp.json @@ -44,24 +44,16 @@ "app_name": "アプリ名", "app_password": "アプリパスワードを追加", "app_passwd_protocols": "アプリパスワードの許可プロトコル", - "automap": "フォルダーを自動マッピングする試み(例:「送信済みアイテム」、「送信済み」=>「送信済み」)", "backup_mx_options": "リレーオプション", "bcc_dest_format": "BCC送信先は単一の有効なメールアドレスである必要があります。
複数のアドレスにコピーを送信する必要がある場合は、エイリアスを作成し、ここで使用してください。", "comment_info": "プライベートコメントはユーザーには表示されませんが、パブリックコメントはユーザーの概要でホバーするとツールチップとして表示されます。", - "custom_params": "カスタムパラメータ", - "custom_params_hint": "正: --param=xy, 誤: --param xy", - "delete1": "完了時にソースから削除", - "delete2": "ソースに存在しないメッセージを送信先で削除", - "delete2duplicates": "送信先で重複を削除", "description": "説明", "destination": "送信先", "disable_login": "ログインを禁止(受信メールは許可)", "domain": "ドメイン", "domain_matches_hostname": "ドメイン %s はホスト名と一致します", "domain_quota_m": "ドメインの総クォータ(MiB)", - "dry": "同期をシミュレーション", "enc_method": "暗号化方式", - "exclude": "オブジェクトを除外(正規表現)", "full_name": "フルネーム", "gal": "グローバルアドレスリスト", "gal_info": "GALにはドメインのすべてのオブジェクトが含まれ、どのユーザーも編集できません。無効にすると、SOGoの空き/取り込み情報が表示されなくなります!変更を適用するにはSOGoを再起動してください。", @@ -77,7 +69,6 @@ "mailbox_username": "ユーザー名(メールアドレスの左部分)", "max_aliases": "作成可能なエイリアスの最大数", "max_mailboxes": "作成可能なメールボックスの最大数", - "mins_interval": "ポーリング間隔(分)", "multiple_bookings": "複数の予約", "nexthop": "次のホップ", "password": "パスワード", @@ -97,16 +88,10 @@ "select_domain": "最初にドメインを選択してください", "sieve_desc": "簡単な説明", "sieve_type": "フィルタータイプ", - "skipcrossduplicates": "フォルダー間で重複するメッセージをスキップ(先着順)", - "subscribeall": "すべてのフォルダーを購読", - "syncjob": "同期ジョブを追加", - "syncjob_hint": "パスワードはプレーンテキストで保存される必要があることに注意してください!", "tags": "タグ", "target_address": "宛先アドレス", "target_address_info": "完全なメールアドレス(カンマ区切り)。", "target_domain": "ターゲットドメイン", - "timeout1": "リモートホストへの接続タイムアウト", - "timeout2": "ローカルホストへの接続タイムアウト", "username": "ユーザー名", "validate": "検証", "validation_success": "正常に検証されました" @@ -585,7 +570,6 @@ "app_name": "アプリ名", "app_passwd": "アプリパスワード", "app_passwd_protocols": "アプリパスワードで許可されるプロトコル", - "automap": "フォルダーを自動マッピングする(例:\"Sent items\", \"Sent\" => \"Sent\" など)", "backup_mx_options": "リレーオプション", "bcc_dest_format": "BCC送信先は1つの有効なメールアドレスである必要があります。
複数のアドレスにコピーを送信する必要がある場合は、エイリアスを作成してここで使用してください。", "client_id": "クライアントID", @@ -593,9 +577,6 @@ "comment_info": "プライベートコメントはユーザーには表示されませんが、パブリックコメントはユーザー概要でホバーするとツールチップとして表示されます。", "created_on": "作成日時", "custom_attributes": "カスタム属性", - "delete1": "完了時にソースから削除", - "delete2": "ソースに存在しないメッセージを送信先で削除", - "delete2duplicates": "送信先で重複を削除", "delete_ays": "削除プロセスを確認してください。", "description": "説明", "disable_login": "ログインを禁止(受信メールは許可)", @@ -619,7 +600,6 @@ "dont_check_sender_acl": "ドメイン %s(およびエイリアスドメイン)の送信者チェックを無効化", "edit_alias_domain": "エイリアスドメインを編集", "encryption": "暗号化", - "exclude": "オブジェクトを除外(正規表現)", "extended_sender_acl": "外部送信者アドレス", "extended_sender_acl_info": "可能であればDKIMドメインキーをインポートしてください。
\r\n このサーバーを対応するSPF TXTレコードに追加することを忘れないでください。
\r\n このサーバーに外部アドレスと重複するドメインまたはエイリアスドメインを追加するたびに、外部アドレスは削除されます。
\r\n *@domain.tld として送信を許可するには @domain.tld を使用してください。", "force_pw_update": "次回ログイン時にパスワードの更新を強制", @@ -646,10 +626,7 @@ "max_aliases": "最大エイリアス数", "max_mailboxes": "作成可能な最大メールボックス数", "max_quota": "メールボックスごとの最大クォータ(MiB)", - "maxage": "リモートから取得するメッセージの最大日数
(0 = 日数を無視)", - "maxbytespersecond": "毎秒の最大バイト数
(0 = 無制限)", "mbox_rl_info": "このレート制限はSASLログイン名に適用され、ログインしたユーザーによって使用されるすべての「送信者」アドレスに一致します。メールボックスレート制限は、ドメイン全体のレート制限を上書きします。", - "mins_interval": "間隔(分)", "multiple_bookings": "複数予約", "none_inherit": "なし / 継承", "nexthop": "次のホップ", @@ -690,7 +667,6 @@ "sender_acl_info": "メールボックスユーザーAがメールボックスユーザーBとして送信することを許可されている場合、SOGoで送信者アドレスが自動的に選択可能な「送信者」フィールドとして表示されることはありません。
\r\n メールボックスユーザーBは、SOGoでメールボックスを委任する必要があります。この委任は、メールビューの左上にあるメールボックス名の右にあるメニュー(三点ボタン)を使用して行います。この動作はエイリアスアドレスには適用されません。", "sieve_desc": "簡単な説明", "sieve_type": "フィルタータイプ", - "skipcrossduplicates": "フォルダー間で重複するメッセージをスキップ(先着順)", "sogo_access": "SOGoへの直接ログインアクセスを許可", "sogo_access_info": "メールUI内からのシングルサインオンは引き続き動作します。この設定は他のすべてのサービスへのアクセスには影響しません。また、ユーザーの既存のSOGoプロファイルを削除または変更するものでもありません。", "sogo_visible": "SOGoにエイリアスが表示される", @@ -699,12 +675,8 @@ "spam_filter": "スパムフィルター", "spam_policy": "ホワイトリスト/ブラックリストにアイテムを追加または削除", "spam_score": "カスタムスパムスコアを設定", - "subfolder2": "送信先でサブフォルダーに同期
(空欄 = サブフォルダーを使用しない)", - "syncjob": "同期ジョブを編集", "target_address": "転送先アドレス(カンマ区切り)", "target_domain": "ターゲットドメイン", - "timeout1": "リモートホストへの接続タイムアウト", - "timeout2": "ローカルホストへの接続タイムアウト", "title": "オブジェクトを編集", "unchanged_if_empty": "変更がない場合は空欄にしてください", "username": "ユーザー名", @@ -915,16 +887,6 @@ "spam_aliases": "一時的なエイリアス", "stats": "統計", "status": "ステータス", - "sync_jobs": "同期ジョブ", - "syncjob_check_log": "ログを確認", - "syncjob_last_run_result": "最後の実行結果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "接続問題", - "syncjob_EXIT_TLS_FAILURE": "暗号化接続の問題", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "認証問題", - "syncjob_EXIT_OVERQUOTA": "宛先メールボックスがクォータを超過", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "リモートサーバーに接続できません", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "ユーザー名またはパスワードが間違っています", "table_size": "テーブルサイズ", "table_size_show_n": "%s 件を表示", "target_address": "転送先アドレス", @@ -1154,6 +1116,37 @@ "waiting_usb_register": "USBデバイスを待っています...

上記にパスワードを入力し、USBデバイスのボタンを押して登録を確認してください。", "yubi_otp": "Yubico OTP認証" }, + "syncjobs": { + "sync_jobs": "同期ジョブ", + "syncjob_check_log": "ログを確認", + "syncjob_last_run_result": "最後の実行結果", + "syncjob_EX_OK": "成功", + "syncjob_EXIT_CONNECTION_FAILURE": "接続問題", + "syncjob_EXIT_TLS_FAILURE": "暗号化接続の問題", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "認証問題", + "syncjob_EXIT_OVERQUOTA": "宛先メールボックスがクォータ超過", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "リモートサーバーに接続できません", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "ユーザー名またはパスワードが間違っています", + "add": "同期ジョブを追加", + "add_hint": "パスワードはプレーンテキストで保存される必要があることに注意してください!", + "edit": "同期ジョブを編集", + "create": "新しい同期ジョブを作成", + "dry": "同期をシミュレーション", + "subfolder2": "送信先でサブフォルダーに同期
(空欄 = サブフォルダーを使用しない)", + "mins_interval": "ポーリング間隔(分)", + "maxage": "リモートから取得するメッセージの最大日数
(0 = 日数を無視)", + "exclude": "オブジェクトを除外(正規表現)", + "automap": "フォルダーを自動マッピングする試み(例:「送信済みアイテム」、「送信済み」=>「送信済み」)", + "delete1": "完了時にソースから削除", + "delete2": "ソースに存在しないメッセージを送信先で削除", + "delete2duplicates": "送信先で重複を削除", + "skipcrossduplicates": "フォルダー間で重複するメッセージをスキップ(先着順)", + "subscribeall": "すべてのフォルダーを購読", + "timeout1": "リモートホストへの接続タイムアウト", + "timeout2": "ローカルホストへの接続タイムアウト", + "maxbytespersecond": "毎秒の最大バイト数
(0 = 無制限)", + "custom_params": "カスタムパラメータ" + }, "user": { "action": "アクション", "active": "有効", @@ -1183,7 +1176,6 @@ "clear_recent_successful_connections": "最近の成功した接続をクリア", "client_configuration": "メールクライアントとスマートフォンの設定ガイドを表示", "create_app_passwd": "アプリパスワードを作成", - "create_syncjob": "新しい同期ジョブを作成", "created_on": "作成日", "daily": "毎日", "day": "日", @@ -1289,16 +1281,6 @@ "spamfilter_wl_desc": "ホワイトリストに登録されたメールアドレスは絶対にスパムとして分類されません。ワイルドカードを使用できます。フィルターは直接エイリアス(単一ターゲットメールボックスを持つエイリアス)にのみ適用され、キャッチオールエイリアスやメールボックス自体には適用されません。", "spamfilter_yellow": "黄: このメッセージはスパムの可能性があり、スパムとしてタグ付けされ、迷惑メールフォルダに移動されます", "status": "ステータス", - "sync_jobs": "同期ジョブ", - "syncjob_check_log": "ログを確認", - "syncjob_last_run_result": "最後の実行結果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "接続問題", - "syncjob_EXIT_TLS_FAILURE": "暗号化接続の問題", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "認証問題", - "syncjob_EXIT_OVERQUOTA": "宛先メールボックスがクォータ超過", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "リモートサーバーに接続できません", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "ユーザー名またはパスワードが間違っています", "tag_handling": "タグ付きメールの処理を設定", "tag_help_example": "タグ付きメールアドレスの例: me+Facebook@example.org", "tag_help_explain": "サブフォルダー: タグ名に基づいて新しいサブフォルダーがINBOXの下に作成されます(例: \"INBOX/Facebook\")。
\r\n件名: タグ名がメールの件名に追加されます(例: \"[Facebook] My News\")。", diff --git a/data/web/lang/lang.ko-kr.json b/data/web/lang/lang.ko-kr.json index 684339b18..58a16c39d 100644 --- a/data/web/lang/lang.ko-kr.json +++ b/data/web/lang/lang.ko-kr.json @@ -43,14 +43,8 @@ "alias_domain_info": "유효한 도메인 명만 입력 (쉼표로 구분).", "app_name": "앱 이름", "app_password": "앱 비밀번호 추가", - "automap": "자동으로 폴더 연결 시도하기 (\"Sent items\", \"Sent\" => \"Sent\" 등등)", "backup_mx_options": "예비 MX 설정", "comment_info": "비공개 메모는 사용자에게 보이지 않지만, 공개 메모는 사용자 개요에서 툴팁 형태로 볼 수 있습니다.", - "custom_params": "추가 변수", - "custom_params_hint": "올바른 설정: --param=xy, 올바르지 않은 설정: --param xy", - "delete1": "완료하면 원래 있던 곳에서 삭제", - "delete2": "원래 있는 곳에 있지 않는 메일을 도착지에서 삭제", - "delete2duplicates": "도착지에 있는 중복 메일 삭제", "description": "설명", "destination": "목적지", "disable_login": "로그인 비활성화 (메일은 계속 받습니다.)", @@ -58,7 +52,6 @@ "domain_matches_hostname": "Domain %s matches hostname", "domain_quota_m": "도메인에 할당할 디스크 크기 (MiB)", "enc_method": "암호화 방식", - "exclude": "제외할 항목 (정규식)", "full_name": "이름", "gal": "글로벌 주소 리스트", "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! Restart SOGo to apply changes.", @@ -74,7 +67,6 @@ "mailbox_username": "사용자 이름 (이메일의 @ 왼쪽 부분)", "max_aliases": "최대 별칭 주소", "max_mailboxes": "최대 메일함 수", - "mins_interval": "폴링 간격 (단위: 분)", "multiple_bookings": "Multiple bookings", "nexthop": "Next hop", "password": "비밀번호", @@ -94,15 +86,9 @@ "select_domain": "도메인을 먼저 선택해주세요.", "sieve_desc": "짧은 설명", "sieve_type": "필터 종류", - "skipcrossduplicates": "폴더들의 중복 메세지 건너뛰기 (first come, first serve)", - "subscribeall": "모든 폴더 구독", - "syncjob": "동기화 작업 추가", - "syncjob_hint": "비밀번호가 일반 텍스트로 저장됨을 주의해주세요.", "target_address": "타깃 주소", "target_address_info": "완전한 이메일 주소 (쉼표로 구분).", "target_domain": "타깃 도메인", - "timeout1": "원격 호스트 연결 시간 초과", - "timeout2": "로컬 호스트 연결 시간 초과", "username": "사용자명", "validate": "확인하기", "validation_success": "성공적으로 확인됨", @@ -544,15 +530,11 @@ "alias": "Edit alias", "app_name": "App name", "app_passwd": "App password", - "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Backup MX options", "bcc_dest_format": "BCC destination must be a single valid email address.", "client_id": "Client ID", "client_secret": "Client secret", "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a users overview", - "delete1": "Delete from source when completed", - "delete2": "Delete messages on destination that are not on source", - "delete2duplicates": "Delete duplicates on destination", "delete_ays": "Please confirm the deletion process.", "description": "Description", "disable_login": "Disallow login (incoming mail is still accepted)", @@ -563,7 +545,6 @@ "dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)", "edit_alias_domain": "Edit Alias domain", "encryption": "Encryption", - "exclude": "Exclude objects (regex)", "extended_sender_acl": "External sender addresses", "extended_sender_acl_info": "A DKIM domain key should be imported, if available.
\r\n Remember to add this server to the corresponding SPF TXT record.
\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.
\r\n Use @domain.tld to allow to send as *@domain.tld.", "force_pw_update": "Force password update at next login", @@ -582,10 +563,7 @@ "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", - "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", - "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", "mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.", - "mins_interval": "Interval (min)", "multiple_bookings": "Multiple bookings", "nexthop": "Next hop", "password": "Password", @@ -619,18 +597,13 @@ "sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.
\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.", "sieve_desc": "Short description", "sieve_type": "Filter type", - "skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)", "sogo_visible": "Alias is visible in SOGo", "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.", "spam_alias": "Create or change time limited alias addresses", "spam_policy": "Add or remove items to white-/blacklist", "spam_score": "Set a custom spam score", - "subfolder2": "Sync into subfolder on destination
(empty = do not use subfolder)", - "syncjob": "Edit sync job", "target_address": "Goto address/es (comma-separated)", "target_domain": "Target domain", - "timeout1": "Timeout for connection to remote host", - "timeout2": "Timeout for connection to local host", "title": "Edit object", "unchanged_if_empty": "If unchanged leave blank", "username": "Username", @@ -798,7 +771,6 @@ "spam_aliases": "Temp. alias", "stats": "Statistics", "status": "Status", - "sync_jobs": "Sync jobs", "table_size": "Table size", "table_size_show_n": "Show %s items", "target_address": "Goto address", @@ -978,6 +950,27 @@ "waiting_usb_register": "Waiting for USB device...

Please enter your password above and confirm your WebAuthn registration by tapping the button on your WebAuthn USB device.", "yubi_otp": "Yubico OTP authentication" }, + "syncjobs": { + "sync_jobs": "동기화 작업", + "add": "동기화 작업 추가", + "add_hint": "비밀번호가 일반 텍스트로 저장됨을 주의해주세요.", + "edit": "Edit sync job", + "create": "새 동기화 작업 생성", + "subfolder2": "Sync into subfolder on destination
(empty = do not use subfolder)", + "mins_interval": "폴링 간격 (단위: 분)", + "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", + "exclude": "제외할 항목 (정규식)", + "automap": "자동으로 폴더 연결 시도하기 (\"Sent items\", \"Sent\" => \"Sent\" 등등)", + "delete1": "완료하면 원래 있던 곳에서 삭제", + "delete2": "원래 있는 곳에 있지 않는 메일을 도착지에서 삭제", + "delete2duplicates": "도착지에 있는 중복 메일 삭제", + "skipcrossduplicates": "폴더들의 중복 메세지 건너뛰기 (first come, first serve)", + "subscribeall": "모든 폴더 구독", + "timeout1": "원격 호스트 연결 시간 초과", + "timeout2": "로컬 호스트 연결 시간 초과", + "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", + "custom_params": "추가 변수" + }, "user": { "action": "조치", "active": "활성화됨", @@ -1002,7 +995,6 @@ "change_password": "비밀번호 변경", "client_configuration": "이메일 클라이언트와 스마트폰을 위한 계정 설정 가이드 보기", "create_app_passwd": "앱 비밀번호 생성하기", - "create_syncjob": "새 동기화 작업 생성", "daily": "매일", "day": "일", "description": "Description", @@ -1082,7 +1074,6 @@ "spamfilter_wl_desc": "화이트리스트에 등록된 메일로부터 온 메세지들은 절대로 스팸으로 분류되지 않습니다. 와일드카드를 사용할 수 있습니다. 필터는 1대1로 대응하는 이메일 별칭에만 적용되며, catch-all 형식의 별칭과 메일함 자체는 제외합니다.", "spamfilter_yellow": "노랑: 이 메세지는 스팸일 수 있으며, 메일이 스팸함으로 이동합니다.", "status": "상태", - "sync_jobs": "동기화 작업", "tag_handling": "태그가 있는 메일 처리 방식 설정", "tag_help_example": "태그가 있는 이메일 주소의 예시: me+Facebook@example.org", "tag_help_explain": "하위 폴더로 이동: INBOX 아래에 태그와 같은 이름을 가진 새 하위 폴더를 생성합니다. (\"INBOX/Facebook\").
\r\n제목에 추가: 태그가 메일 제목 앞에 추가됩니다. 예시: \"[Facebook] 새 소식\".", diff --git a/data/web/lang/lang.lt-lt.json b/data/web/lang/lang.lt-lt.json index 78e48ea13..8f21569f4 100644 --- a/data/web/lang/lang.lt-lt.json +++ b/data/web/lang/lang.lt-lt.json @@ -38,15 +38,11 @@ "app_name": "Programėlės pavadinimas", "app_password": "Pridėti programėlės slaptažodį", "app_passwd_protocols": "Prieinamieji programėlės slaptažodžio protokolai", - "custom_params": "Pasirinktiniai parametrai", - "delete1": "Ištrinti iš šaltinio, kai baigta", "activate_filter_warn": "Visi kiti filtrai bus deaktivuoti, kai „aktyvus“ yra pražymėtas.", - "custom_params_hint": "Teisingai: --param=xy, neteisingai: --param xy", "description": "Aprašymas", "destination": "Gavėjas", "domain": "Domenas", "domain_quota_m": "Viso domeno kvota (MiB)", - "dry": "Simuliuoti sinchronizaciją", "enc_method": "Šifravimo metodas", "full_name": "Prilnas vardas", "generate": "generuoti", @@ -64,13 +60,9 @@ "alias_address": "Pakaitiniai/is adresai/as", "alias_address_info": "Pilnas el. pašto adresas (-ai) arba @example.com, kad gautumėte visus pranešimus iš domeno (atskirtus kableliais). Tik \"mailcow\" domenai.", "alias_domain": "Pakaitinis domenas", - "automap": "Stenktis automatiškai susieti aplankus (\"Išsiųsti\", \"Išsiųsti\" => \"išsiųsti\" ir tt.)", "backup_mx_options": "Perdavimo nustatymai", "bcc_dest_format": "BCC gavimo adresas turi būti vienas galiojantis el. pašto adresas.
Jei turite siųsti kopiją į kelis adresus, sukurkite slapyvardį ir naudokite jį čia.", - "delete2": "Ištrinti žinutes gavimo vietoje, kurios nėra šaltinyje.", - "delete2duplicates": "Ištrinti dublikatus gavimo vietoje", "domain_matches_hostname": "Domenas %s atitinka serverio vardą", - "exclude": "Išskirti objektus (regex)", "gal": "Visuotinis adresų sąrašas", "gal_info": "GAL (Global Address List) apima visus domeno objektus ir jų negali redaguoti joks vartotojas. SOGo trūksta laisvosios/užimtosios informacijos, jei tai išjungta! Perkraukite SOGo, kad pritaikytumėte pakeitimus." }, @@ -350,5 +342,14 @@ "dovecot_restart_success": "„Doveccot“ perkrautas", "domain_added": "Pridėtas domenas %s", "domain_admin_modified": "Pakeitimai domeno adminustratoriui %s išsaugoti" + }, + "syncjobs": { + "dry": "Simuliuoti sinchronizaciją", + "exclude": "Išskirti objektus (regex)", + "automap": "Stenktis automatiškai susieti aplankus (\"Išsiųsti\", \"Išsiųsti\" => \"išsiųsti\" ir tt.)", + "delete1": "Ištrinti iš šaltinio, kai baigta", + "delete2": "Ištrinti žinutes gavimo vietoje, kurios nėra šaltinyje.", + "delete2duplicates": "Ištrinti dublikatus gavimo vietoje", + "custom_params": "Pasirinktiniai parametrai" } } diff --git a/data/web/lang/lang.lv-lv.json b/data/web/lang/lang.lv-lv.json index df1697282..d52d5daf1 100644 --- a/data/web/lang/lang.lv-lv.json +++ b/data/web/lang/lang.lv-lv.json @@ -37,16 +37,11 @@ "alias_address_info": "Pilna epasta adrese/s vai @example.com, lai notvertu visus domēna ziņojumus (atdalītas ar komatu). Tikai mailcow domēni.", "alias_domain": "Aizstājdomēni", "alias_domain_info": "Tikai derīgi domēna vārdi (komatu atdalīti).", - "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)", "backup_mx_options": "Releja iespējas", - "delete1": "Izdzēst no avota pēc pabeigšanas", - "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", - "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", "description": "Apraksts", "domain": "Domēns", "domain_quota_m": "Kopējā domēna kvota (MiB)", "enc_method": "Šifrēšanas metode", - "exclude": "Izslēgt objektus (regex)", "full_name": "Pilns vārds", "goto_null": "Klusām atmest pastu", "hostname": "Saimniekdators", @@ -55,7 +50,6 @@ "mailbox_username": "Lietotājvārds (kriesā daļa no epasta adreses)", "max_aliases": "Lielākais pieļaujamo aizstājvārdu skaits", "max_mailboxes": "Maks. iespējamās pastkastes", - "mins_interval": "Aptauajs intervāls (minūtes)", "multiple_bookings": "Vairākas rezervācijas", "password": "Parole", "password_repeat": "Apstiprinājuma parole (atkārtot)", @@ -69,9 +63,6 @@ "select_domain": "Lūgums vispirms atlasīt domēnu", "sieve_desc": "Īss apraksts", "sieve_type": "Filtra tips", - "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", - "syncjob": "Pievienot sinhronizācijas darbu", - "syncjob_hint": "Ņemiet vērā, ka parole ir jāuzglabā vienkāršā tekstā!", "target_address": "Mērķa adreses", "target_address_info": "Pilna epasta adrese/s (atdalītas ar komatu).", "target_domain": "Mērķa domēns", @@ -269,11 +260,7 @@ "edit": { "active": "Aktīvs", "alias": "Labot aizstājvārdu", - "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" utt.)", "backup_mx_options": "Retranslācijas iespējams", - "delete1": "Izdzēst no avota pēc pabeigšanas", - "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", - "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", "description": "Apraksts", "domain": "Labot domēnu", "domain_admin": "Labot domēna pārvaldītāju", @@ -282,7 +269,6 @@ "dont_check_sender_acl": "Atspējot sūtītāju pārbaudi domēnam %s (+ aizstājdomēni)", "edit_alias_domain": "Labot aizstājdomēnu", "encryption": "Šifrēšana", - "exclude": "Neiekļaut objektus (regex)", "force_pw_update": "Piespiedu paroles atjaunošana nākamajā pieteikšanās reizē", "force_pw_update_info": "Šis lietotājs varēs pieteikties tikai %s. Lietotņu paroles joprojām būs izmantojamas.", "full_name": "Pilns vārds", @@ -293,9 +279,6 @@ "max_aliases": "Lielākais aizstājvārdu skaits", "max_mailboxes": "Maks. iespējamās pastkastes", "max_quota": "Maks. kvota uz pastkasti (MiB)", - "maxage": "Lielākais pieļaujamais ziņojumu, kuri tiks vaicāti attālajā serverī, vecums dienās
(0 = neņemt vērā vecumu)", - "maxbytespersecond": "Maks. baiti sekundē (0 ir vienāds ar neierobežotu skaitu)", - "mins_interval": "Intervāls (min)", "multiple_bookings": "Vairāki rezervējumi", "password": "Parole", "password_repeat": "Apstiprinājuma parole (atkārtot)", @@ -310,12 +293,9 @@ "sender_acl": "Atļaut sūtīt kā", "sieve_desc": "Īss apraksts", "sieve_type": "Filtra tips", - "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", "spam_alias": "Izveidot vai mainīt laika ierobežotas aizstājadreses", "spam_policy": "Pievienot vai noņemt vienumus atļautajā/liegumu sarakstā", "spam_score": "Iestatīt pielāgotu mēstules vērtējumu", - "subfolder2": "Sinhronizēt galamērķa apakšmapē
(tukšs = neizmantot apakšmapi)", - "syncjob": "Labot sinhronizācijas darbu", "target_address": "Mērķa adrese/s (atdalītas ar komatu)", "target_domain": "Mērķa domēns", "title": "Labot priekšmetu", @@ -457,7 +437,6 @@ "sieve_info": "Katram lietotājam var saglabāt vairākus filtrus, bet tikai viens pirmsfiltrs un viens pēcfiltrs var darboties vienlaicīgi.
\nKatrs filtrs tiks apstrādāts norādītajā kārtībā. Ne kļūdains skripts, ne izdots \"keep;\" nepārtrauks turpmāko skriptu apstrādi. Vispārējo sieve skriptu izmaiņas izsauks Dovecot pārsāknēšanu.

Vispārējais sieve pirmsfiltrs • Pirmsfiltrs • Lietotāja skripti • Pēcfiltrs • Vispārējais sieve pēcfiltrs", "spam_aliases": "Pagaidu aizstājvārds", "status": "Status", - "sync_jobs": "Sinhronizācijas darbi", "target_address": "Doties uz adresi", "target_domain": "Mērķa domēns", "tls_enforce_in": "Piespiest TLS ienākošajiem", @@ -481,8 +460,7 @@ "alias_domain_alias_hint": "Aizstājvārdi netiek automātiski piemēroti domēnu aizstājvārdiem. Aizstājadrese my-alias@domain nenosedz adresi my-alias@alias-domain (kur \"alias-domain\" ir iedomāts \"domain\" aizstājdomēns).
Lūgums izmantot sieta atlasi, lai pārvirzītu pastu uz ārēju pastkasti (skatīt cilti \"Atlasīšana\" vai izmantot SOGo -> Pārsūtītājs). \"Izvērst aizstājvārdu pār aizstājdomēniem\" ir izmantojams, lai automātiski pievienotu trūkstošos aiztājvārdus.", "alias_domain_backupmx": "Aizstājdomēns ir neaktīvs retranslācijas domēnam", "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)", - "sieve_preset_1": "Atmest e-pasta vēstules ar iespējami bīstamiem datņu veidiem", - "syncjob_last_run_result": "Pēdējās izpildes iznākums" + "sieve_preset_1": "Atmest e-pasta vēstules ar iespējami bīstamiem datņu veidiem" }, "quarantine": { "action": "Darbības", @@ -592,6 +570,24 @@ "authenticators": "Autentificētāji", "u2f_deprecated_important": "Lūgums reģistrēt savu atslēgu pārvaldības lapā ar jauno WebAuthn veidu." }, + "syncjobs": { + "sync_jobs": "Sinhronizācijas uzdevumi", + "syncjob_last_run_result": "Pēdējās izpildes iznākums", + "add": "Pievienot sinhronizācijas darbu", + "add_hint": "Ņemiet vērā, ka parole ir jāuzglabā vienkāršā tekstā!", + "edit": "Labot sinhronizācijas darbu", + "create": "Izveidot jaunu sinhronizācijas darbu", + "subfolder2": "Sinhronizēt galamērķa apakšmapē
(tukšs = neizmantot apakšmapi)", + "mins_interval": "Aptauajs intervāls (minūtes)", + "maxage": "Lielākais pieļaujamais ziņojumu, kuri tiks vaicāti attālajā serverī, vecums dienās
(0 = neņemt vērā vecumu)", + "exclude": "Izslēgt objektus (regex)", + "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)", + "delete1": "Izdzēst no avota pēc pabeigšanas", + "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", + "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", + "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", + "maxbytespersecond": "Maks. baiti sekundē (0 ir vienāds ar neierobežotu skaitu)" + }, "user": { "action": "Rīcība", "active": "Aktīvs", @@ -608,7 +604,6 @@ "aliases_send_as_all": "Nepārbaudīt sūtītāja piekļuvi šiem domēniem un to aizstājdomēniem", "change_password": "Nomainīt paroli", "client_configuration": "Parādīt konfigurācijas norādes e-pasta klientiem un tālruņiem", - "create_syncjob": "Izveidot jaunu sinhronizācijas darbu", "day": "Dienas", "description": "Apraksts", "direct_aliases": "Tiešas aizstājadreses", @@ -659,7 +654,6 @@ "spamfilter_wl_desc": "No atļautā saraksta e-pasta adresēm saņemtās vēstules nekad netiks atzīmētas kā mēstules. Var tikt izmantotas aizstājzīmes. Atlase tiek piemērota tikai tiešiem aizstājvārdiem (aizstājvārdiem ar vienu mērķa pastkasti), izņemot visu tverošos aizstājvārdus un pašu pastkasti.", "spamfilter_yellow": "Dzeltens: šī vēstule visticamāk ir spams un tiks pārvietota uz Junk mapi", "status": "Status", - "sync_jobs": "Sinhronizācijas uzdevumi", "tag_handling": "Iestatīt apstrādi par atzīmētajiem e-pastiem", "tag_help_example": "Piemērs ietagotai e-pasta adresei: me+Facebook@example.org", "tag_help_explain": "Iekš apakšmapes: Jaunas apakšmapes, kas izveidotas pēc taga, tiks izveidotas zem INBOX (\"INBOX/Facebook\").
\r\nSubjektā: Tagu nosaukums tiks pievienots e-pasta ziņojumam, piemēram: \"[Facebook] Manas ziņas\".", @@ -693,8 +687,7 @@ "create_app_passwd": "Izveidot lietotnes paroli", "empty": "Nav iznākuma", "quarantine_notification_info": "Tiklīdz paziņojums ir nosūtīts, vienumi tiks atzīmēti kā \"paziņoti\", un par šo vienumu vairs netiks sūtīti paziņojumi.", - "sender_acl_disabled": "Sūtītāja pārbaude ir atspējota", - "syncjob_last_run_result": "Pēdējās izpildes iznākums" + "sender_acl_disabled": "Sūtītāja pārbaude ir atspējota" }, "datatables": { "paginate": { diff --git a/data/web/lang/lang.nb-no.json b/data/web/lang/lang.nb-no.json index bca608745..d8211abb2 100644 --- a/data/web/lang/lang.nb-no.json +++ b/data/web/lang/lang.nb-no.json @@ -196,28 +196,19 @@ "app_passwd_protocols": "Tillatte protokoller for app-passord", "hostname": "Vert", "goto_ham": "Lær som ham", - "custom_params": "Tilpassede parametre", "target_address": "Gå-til-adresse", - "delete2": "Slett meldinger på måldestinasjonen som ikke finnes på kilden", - "syncjob": "Legg til synkroniseringsjobb", "domain": "Domene", - "timeout1": "Tidsavbrudd for tilkobling til ekstern vert", "goto_null": "Slett epost uten varsling", - "custom_params_hint": "Riktig: --param=xy, feil: --param xy", "multiple_bookings": "Flere reservasjoner", "alias_domain": "Aliasdomene", - "exclude": "Ekskludér objekter (regex)", "validate": "Validér", - "dry": "Simulér synkronisering", "relay_all_info": "↪ Hvis du velger å ikke videresende for alle mottakere, så må du legge til en (\"blind\") postkasse for hver eneste mottaker som det skal videresendes for.", "description": "Beskrivelse", "destination": "Måldestinasjon", "alias_domain_info": "Kun gyldige domenenavn (kommaseparerte).", "mailbox_username": "Brukernavn (venstre del av en epostadresse)", "username": "Brukernavn", - "mins_interval": "Sjekkintervall (minutter)", "disable_login": "Ikke tillat innlogging (innkommende epost blir fremdeles mottatt)", - "delete2duplicates": "Slett duplikater på måldestinasjonen", "post_domain_add": "SOGo-kontaineren, \"sogo-mailcow\", må startes på nytt etter at man har lagt til et nytt domene!

I tillegg må domenets DNS-oppsett ses gjennom. Når DNS-oppsettet er godkjent, start \"acme-mailcow\" på nytt for å automatisk opprette sertifikater for det nye domenet (autoconfig.<domain>, autodiscover.<domain>).
Dette steget er valgfritt og vil bli forsøkt utført hver 24. time.", "select": "Vennligst velg...", "full_name": "Fullt navn", @@ -240,7 +231,6 @@ "inactive": "Inaktiv", "domain_quota_m": "Total domenekvote (MiB)", "password": "Passord", - "delete1": "Slett fra kilde når fullført", "tags": "Emneknagger", "port": "Port", "backup_mx_options": "Alternativer for videresending", @@ -249,20 +239,15 @@ "goto_spam": "Lær som spam", "target_domain": "Måldomene", "public_comment": "Offentlig kommentar", - "timeout2": "Tidsavbrudd for tilkobling til lokal vert", "mailbox_quota_def": "Standard kvote for postkasser", - "skipcrossduplicates": "Hopp over duplikatmeldinger på tvers av mapper (førstemann til mølla)", "sieve_desc": "Kort beskrivelse", "mailbox_quota_m": "Maks. kvote pr. postkasse (MiB)", - "automap": "Forsøk automatisk mappe-tilordning (\"Sendte elementer\", \"Sendt\" => \"Sendt\" etc.)", "validation_success": "Validering fullført", "relay_all": "Videresend for alle mottakere", - "subscribeall": "Abonnér på alle mapper", "domain_matches_hostname": "Domene %s er det samme som vertsnavn", "generate": "opprett", "app_password": "Legg til app-passord", "max_mailboxes": "Maks. antall mulige postkasser", - "syncjob_hint": "Vær oppmerksom på at passord må lagres i klartekst!", "relayhost_wrapped_tls_info": "Vennligst ikke bruk TLS-aktiverte porter (stort sett bare brukt på port 465).
\nBruk en annen ikke-TLS-aktivert port og send STARTTLS. En TLS-policy for å tvinge TLS kan opprettes i \"TLS-policy-mappinger\".", "relay_unknown_only": "Videresend kun for ikke-eksisterende postkasser. Eksisterende postkasser vil bli levert lokalt.", "sieve_type": "Filtertype", @@ -360,7 +345,6 @@ "bcc_dest_format": "BCC-destinasjon må være en enkelt, gyldig epostadresse.
Hvis du trenger å sende en kopi til flere adresser, opprett et alias og bruk det her.", "pushover_info": "Innstillinger for pushvarslinger vil gjelde alle rene (ikke-spam) eposter levert til %s, inkludert aliaser (delte, ikke-delte, taggede).", "relay_transport_info": "
Info
Du kan definere transportmappinger for et spesifikt mål for dette domenet. Hvis dette ikke er definert, blir det gjort et MX-oppslag.", - "delete2duplicates": "Slett duplikater på målvert", "description": "Beskrivelse", "disable_login": "Ikke tillat innlogging (innkommende epost blir likevel mottatt)", "domain_admin": "Endre domeneadministrator", @@ -387,7 +371,6 @@ "allow_from_smtp_info": "La stå tom for å tillate alle avsendere.
IPv4/IPv6-adresser og -nettverk.", "domain": "Endre domene", "encryption": "Kryptering", - "exclude": "Ekskluder objekter (regex)", "footer_exclude": "Ekskluder fra bunntekst", "gal_info": "GAL inneholder alle objeker i et domene og kan ikke redigeres av noen brukere. Ledig/opptatt-informasjon i SOGo mangler dersom den deaktiveres! Start SOGo på nytt for å aktivere endringene.", "grant_types": "Grant-typer", @@ -399,8 +382,6 @@ "mailbox": "Endre mailboks", "mailbox_quota_def": "Standardkvote for mailboks", "max_quota": "Maks kvote pr. mailboks (MiB)", - "maxage": "Maksimal alder for meldinger, i dager, som vil hentes fra ekstern
(0 = ignorer alder)", - "maxbytespersecond": "Maks bytes pr. sekund
(0 = ubegrenset)", "pushover_sender_array": "Bare vurder de følgende avsenderadressene (komma-separert)", "pushover_sender_regex": "Vurder følgende avsender-regex", "pushover_text": "Varslingstekst", @@ -416,7 +397,6 @@ "full_name": "Fullt navn", "gal": "Global adresseliste", "max_aliases": "Maks. antall aliaser", - "mins_interval": "Intervall (min)", "multiple_bookings": "Flere bookinger", "none_inherit": "Ingen / arve", "acl": "ACL (rettighet)", @@ -428,14 +408,11 @@ "app_name": "Appnavn", "app_passwd": "App-passord", "app_passwd_protocols": "Tillatte protokoller for app-passord", - "automap": "Prøv å automatisk mappe opp mapper (\"Sent items\", \"Sent\" => \"Sendt\" etc.)", "backup_mx_options": "Videresendingsalternativer", "client_secret": "Klient-hemmelighet", "comment_info": "En privat kommentar er ikke synlig for brukeren, mens en offentlig kommentar vises som et tooltip når man holder muspekeren over det", "created_on": "Opprettet den", "custom_attributes": "Valgfrie attributter", - "delete1": "Slett fra kilde når fullført", - "delete2": "Slett meldinger på målvert som ikke finnes på kildeverten", "delete_ays": "Vennligst bekreft slettingen.", "domain_footer_html": "HTML-bunntekst", "domain_footer_plain": "PLAIN-bunntekst", @@ -464,5 +441,23 @@ "scope": "Omfang", "sender_acl": "Tillat å sende som", "sender_acl_disabled": "Avsender-sjekk er deaktivert" + }, + "syncjobs": { + "add": "Legg til synkroniseringsjobb", + "add_hint": "Vær oppmerksom på at passord må lagres i klartekst!", + "dry": "Simulér synkronisering", + "mins_interval": "Sjekkintervall (minutter)", + "maxage": "Maksimal alder for meldinger, i dager, som vil hentes fra ekstern
(0 = ignorer alder)", + "exclude": "Ekskludér objekter (regex)", + "automap": "Forsøk automatisk mappe-tilordning (\"Sendte elementer\", \"Sendt\" => \"Sendt\" etc.)", + "delete1": "Slett fra kilde når fullført", + "delete2": "Slett meldinger på måldestinasjonen som ikke finnes på kilden", + "delete2duplicates": "Slett duplikater på måldestinasjonen", + "skipcrossduplicates": "Hopp over duplikatmeldinger på tvers av mapper (førstemann til mølla)", + "subscribeall": "Abonnér på alle mapper", + "timeout1": "Tidsavbrudd for tilkobling til ekstern vert", + "timeout2": "Tidsavbrudd for tilkobling til lokal vert", + "maxbytespersecond": "Maks bytes pr. sekund
(0 = ubegrenset)", + "custom_params": "Tilpassede parametre" } } diff --git a/data/web/lang/lang.nl-nl.json b/data/web/lang/lang.nl-nl.json index 6ed9e322a..66dab5fb6 100644 --- a/data/web/lang/lang.nl-nl.json +++ b/data/web/lang/lang.nl-nl.json @@ -42,14 +42,8 @@ "alias_domain_info": "Uitsluitend geldige domeinnamen (kommagescheiden).", "app_name": "Naam van app", "app_password": "Maak appwachtwoord aan", - "automap": "Probeer mappen automatisch te koppelen (\"Verzonden items\", \"Verzonden\" => \"Verzonden\" etc.)", "backup_mx_options": "Relay-opties", "comment_info": "Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publieke opmerking wel getoond wordt in het overzicht van de gebruiker.", - "custom_params": "Aangepaste parameters", - "custom_params_hint": "Juist: --param=xy, onjuist: --param xy", - "delete1": "Verwijder van bron wanneer voltooid", - "delete2": "Verwijder berichten die zich niet in de bron bevinden", - "delete2duplicates": "Verwijder duplicaten op de bestemming", "description": "Beschrijving", "destination": "Bestemming", "disable_login": "Weiger aanmelden (inkomende mail blijft binnenkomen)", @@ -57,7 +51,6 @@ "domain_matches_hostname": "Domein %s komt overeen met hostname", "domain_quota_m": "Totale domeinquota (MiB)", "enc_method": "Versleutelingsmethode", - "exclude": "Sluit objecten uit (regex)", "full_name": "Volledige naam", "gal": "Globale adreslijst", "gal_info": "De globale adreslijst bevat alle objecten van een domein. Deze kunnen door geen enkele gebruiker worden bewerkt. Herstart SOGo om wijzigingen door te voeren.", @@ -73,7 +66,6 @@ "mailbox_username": "Gebruikersnaam (linkergedeelte van een mailadres)", "max_aliases": "Maximaal aantal aliassen", "max_mailboxes": "Maximaal aantal mailboxen", - "mins_interval": "Interval (min)", "multiple_bookings": "Meerdere boekingen", "nexthop": "Nexthop", "password": "Wachtwoord", @@ -93,21 +85,14 @@ "select_domain": "Selecteer eerst een domein", "sieve_desc": "Korte beschrijving", "sieve_type": "Filtertype", - "skipcrossduplicates": "Sla duplicaten verspreid over mappen over (wie het eerst komt, het eerst maalt)", - "subscribeall": "Abonneer op alle mappen", - "syncjob": "Voeg sync job toe", - "syncjob_hint": "Wees ervan bewust dat de authenticatiedata onversleuteld wordt opgeslagen!", "target_address": "Doeladressen", "target_address_info": "Volledig(e) mailadres(sen) (kommagescheiden).", "target_domain": "Doeldomein", - "timeout1": "Time-out voor verbinding met externe hosts", - "timeout2": "Time-out voor verbinding met lokale hosts", "username": "Gebruikersnaam", "validate": "Verifieer", "validation_success": "Succesvol geverifieerd", "tags": "Tags", "bcc_dest_format": "BCC-bestemming moet één geldig e-mailadres zijn.
Als u een kopie naar meerdere adressen wilt sturen, maak dan een alias aan en gebruik die hier.", - "dry": "Synchronisatie simuleren", "internal": "Intern", "internal_info": "Interne aliassen zijn alleen toegankelijk vanuit het eigen (alias)domein.", "sender_allowed": "Toestaan om te verzenden als deze alias" @@ -613,15 +598,11 @@ "allowed_protocols": "Toegestane protocollen voor directe gebruikerstoegang (heeft geen invloed op protocollen voor app-wachtwoorden).", "app_name": "Naam van app", "app_passwd": "Appwachtwoord", - "automap": "Probeer mappen automatisch te koppelen (\"Verzonden items\", \"Verzonden\" => \"Verzonden\" etc.)", "backup_mx_options": "Relay-opties", "bcc_dest_format": "Een BCC-bestemming dient één geldig mailadres te zijn.", "client_id": "Client-ID", "client_secret": "Client-secret", "comment_info": "Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publieke opmerking wel getoond wordt in het overzicht van de gebruiker.", - "delete1": "Verwijder van bron wanneer voltooid", - "delete2": "Verwijder berichten die zich niet in de bron bevinden", - "delete2duplicates": "Verwijder duplicaten op de bestemming", "delete_ays": "Bevestig de verwijdering.", "description": "Beschrijving", "disable_login": "Weiger aanmelden (inkomende mail blijft binnenkomen)", @@ -632,7 +613,6 @@ "dont_check_sender_acl": "Schakel verzendcontrole uit voor domein %s (inclusief aliasdomeinen)", "edit_alias_domain": "Wijzig aliasdomein", "encryption": "Versleuteling", - "exclude": "Sluit objecten uit (regex)", "extended_sender_acl": "Externe verzendadressen", "extended_sender_acl_info": "Wanneer mogelijk dient er een ARC/DKIM-key geïmporteerd te worden. Vergeet niet om deze server toe te voegen aan het SPF-record
Zodra er een domein of aliasdomein wordt toegevoegd aan deze server, overeenkomend met een extern verzendadres, wordt het externe adres verwijderd.
Gebruik @domain.tld om verzenden vanuit *@domain.tld toe te staan.", "force_pw_update": "Vereis nieuw wachtwoord bij eerstvolgende login", @@ -651,10 +631,7 @@ "max_aliases": "Maximaal aantal aliassen", "max_mailboxes": "Maximaal aantal mailboxen", "max_quota": "Mailboxquota (MiB)", - "maxage": "Maximale leeftijd van berichten (in dagen) die extern worden opgehaald
(0 = negeer leeftijd)", - "maxbytespersecond": "Maximale bytes per seconde
(0 = onbeperkt)", "mbox_rl_info": "De ratelimit wordt toegepast op de huidige mailboxgebruiker en geldt voor elk verzendadres die door deze wordt gebruikt. Een mailbox-ratelimit staat boven een domein-ratelimit.", - "mins_interval": "Interval (min)", "multiple_bookings": "Meerdere boekingen", "nexthop": "Nexthop", "password": "Wachtwoord", @@ -690,19 +667,14 @@ "sender_acl_info": "Wanneer mailboxgebruiker A toegestaan is te verzenden namens mailboxgebruiker B, zal het verzendadres niet automatisch worden weergegeven in het \"van\"-veld in SOGo. Mailboxgebruiker A dient hiervoor een aparte vermelding te maken in SOGo. Om een mailbox te delegeren in SOGo kan het menu (drie punten) aan de rechterkant van de naam van het mailbox linksboven worden gebruikt in de mailweergave. Dit is niet van toepassing op aliasadressen.", "sieve_desc": "Korte beschrijving", "sieve_type": "Filtertype", - "skipcrossduplicates": "Sla duplicaten verspreid over mappen over (wie het eerst komt, het eerst maalt)", "sogo_visible": "Alias tonen in SOGo", "sogo_visible_info": "Wanneer verborgen zal een alias niet worden weergegeven als een selecteerbaar verzendadres. Deze optie beïnvloedt uitsluitend objecten die kunnen worden weergegeven in SOGo (gedeelde of niet-gedeelde aliasadressen die naar minstens één mailbox verwijzen).", "spam_alias": "Maak een nieuw tijdelijk alias aan, of pas deze aan", "spam_filter": "Spamfilter", "spam_policy": "Voeg items toe of verwijder items van de deny- en allowlists", "spam_score": "Stel een aangepaste spamscore in", - "subfolder2": "Synchroniseer in submap op bestemming
(leeg = gebruik geen submappen)", - "syncjob": "Wijzig sync job", "target_address": "Doeladres(sen) (kommagescheiden)", "target_domain": "Doeldomein", - "timeout1": "Time-out voor verbinding met externe hosts", - "timeout2": "Time-out voor verbinding met lokale hosts", "title": "Wijzig object", "unchanged_if_empty": "Laat leeg wanneer ongewijzigd", "username": "Gebruikersnaam", @@ -938,7 +910,6 @@ "spam_aliases": "Tijdelijk alias", "stats": "Statistieken", "status": "Status", - "sync_jobs": "Sync jobs", "table_size": "Tabelgrootte", "table_size_show_n": "Toon %s items", "target_address": "Doeladres", @@ -976,15 +947,6 @@ "recipient": "Ontvanger", "relay_unknown": "Onbekende mailboxen relayen", "sender": "Afzender", - "syncjob_check_log": "Controleer log", - "syncjob_last_run_result": "Laatste uitvoeringsresultaat", - "syncjob_EX_OK": "Succes", - "syncjob_EXIT_CONNECTION_FAILURE": "Verbindingsprobleem", - "syncjob_EXIT_TLS_FAILURE": "Probleem met versleutelde verbinding", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authenticatieprobleem", - "syncjob_EXIT_OVERQUOTA": "Doel mailbox is over quota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kan geen verbinding maken met de externe server", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Verkeerde gebruikersnaam of wachtwoord", "templates": "Sjablonen", "template": "Sjabloon", "yes": "✓" @@ -1187,6 +1149,37 @@ "fido2_validation_failed": "Validatie mislukt", "set_fido2_touchid": "Registreer Touch ID op Apple M1" }, + "syncjobs": { + "sync_jobs": "Sync jobs", + "syncjob_check_log": "Controleer log", + "syncjob_last_run_result": "Laatste uitvoeringsresultaat", + "syncjob_EX_OK": "Succes", + "syncjob_EXIT_CONNECTION_FAILURE": "Verbindingsprobleem", + "syncjob_EXIT_TLS_FAILURE": "Probleem met versleutelde verbinding", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authenticatieprobleem", + "syncjob_EXIT_OVERQUOTA": "Doel mailbox is over quota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kan geen verbinding maken met de externe server", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Verkeerde gebruikersnaam of wachtwoord", + "add": "Voeg sync job toe", + "add_hint": "Wees ervan bewust dat de authenticatiedata onversleuteld wordt opgeslagen!", + "edit": "Wijzig sync job", + "create": "Voeg sync job toe", + "dry": "Synchronisatie simuleren", + "subfolder2": "Synchroniseer in submap op bestemming
(leeg = gebruik geen submappen)", + "mins_interval": "Interval (min)", + "maxage": "Maximale leeftijd van berichten (in dagen) die extern worden opgehaald
(0 = negeer leeftijd)", + "exclude": "Sluit objecten uit (regex)", + "automap": "Probeer mappen automatisch te koppelen (\"Verzonden items\", \"Verzonden\" => \"Verzonden\" etc.)", + "delete1": "Verwijder van bron wanneer voltooid", + "delete2": "Verwijder berichten die zich niet in de bron bevinden", + "delete2duplicates": "Verwijder duplicaten op de bestemming", + "skipcrossduplicates": "Sla duplicaten verspreid over mappen over (wie het eerst komt, het eerst maalt)", + "subscribeall": "Abonneer op alle mappen", + "timeout1": "Time-out voor verbinding met externe hosts", + "timeout2": "Time-out voor verbinding met lokale hosts", + "maxbytespersecond": "Maximale bytes per seconde
(0 = onbeperkt)", + "custom_params": "Aangepaste parameters" + }, "user": { "action": "Handeling", "active": "Actief", @@ -1211,7 +1204,6 @@ "change_password": "Wijzig wachtwoord", "client_configuration": "Toon configuratiegidsen voor mailprogramma's", "create_app_passwd": "Maak appwachtwoord aan", - "create_syncjob": "Voeg sync job toe", "daily": "Dagelijks", "day": "dag", "description": "Beschrijving", @@ -1297,7 +1289,6 @@ "spamfilter_wl_desc": "Zet mailadressen op de whitelist om ze nooit als spam te markeren.
Deze lijst wordt niet toegepast op een gedeeld aliasadres.
Wildcards (*) zijn toegestaan.", "spamfilter_yellow": "Geel: dit bericht is mogelijk spam en zal in de spamfolder geplaatst worden.", "status": "Status", - "sync_jobs": "Sync jobs", "tag_handling": "Mailtags", "tag_help_example": "Voorbeeld van een maildres met tag: me+Tesla@example.org", "tag_help_explain": "In submap: er wordt een nieuwe map aangemaakt, genoemd naar de tag (bijv.: \"INBOX/Tesla\").
In onderwerp: de tag wordt vóór het oorspronkelijke onderwerp geplaatst (bijv.: \"[Tesla] Uw serviceafspraak\").", diff --git a/data/web/lang/lang.pl-pl.json b/data/web/lang/lang.pl-pl.json index de3557c03..b61c60eb7 100644 --- a/data/web/lang/lang.pl-pl.json +++ b/data/web/lang/lang.pl-pl.json @@ -39,13 +39,10 @@ "alias_domain": "Alias domeny", "alias_domain_info": "Tylko prawidłowe nazwy domen (oddzielone przecinkami).", "backup_mx_options": "Opcje Backup MX", - "delete1": "Usuń ze źródła po zakończeniu", - "delete2duplicates": "Usuń duplikaty w miejscu docelowym", "description": "Opis", "domain": "Domena", "domain_quota_m": "Łączny limit domeny (MiB)", "enc_method": "Metoda szyfrowania", - "exclude": "Wyklucz obiekty (regex)", "full_name": "Pełna nazwa", "hostname": "Nazwa hosta", "kind": "Rodzaj", @@ -53,7 +50,6 @@ "mailbox_username": "Nazwa użytkownika (lewa strona adresu email)", "max_aliases": "Maks. liczba aliasów", "max_mailboxes": "Maks. liczba skrzynek", - "mins_interval": "Zakres pobierania (minuty)", "multiple_bookings": "Wielokrotne rejestracje", "password": "Hasło", "password_repeat": "Potwierdź hasło(powtórz)", @@ -65,8 +61,6 @@ "relay_domain": "Domena przekaźnikowa", "select": "Proszę wybrać...", "select_domain": "Proszę najpierw wybrać domenę", - "syncjob": "Dodaj polecenie synchronizacji", - "syncjob_hint": "Pamiętaj, że hasła należy zapisywać w zwykłym tekście!", "target_address": "Adresy Idź do", "target_address_info": " Pełny/e adres/y email (oddzielone przecinkami).", "target_domain": "Domena docelowa", @@ -77,16 +71,11 @@ "app_name": "Nazwa aplikacji", "app_password": "Dodaj hasło do aplikacji", "app_passwd_protocols": "Dozwolone protokoły dla hasła aplikacji", - "automap": "Spróbuj automatycznie mapować foldery (\"Wysłane elementy\", \"Wysłane\" => \"Wysłane\" itp.)", "bcc_dest_format": "Miejscem docelowym BCC musi być jeden prawidłowy adres e-mail.
Jeśli chcesz wysłać kopię do wielu adresów, utwórz alias i użyj go tutaj.", "comment_info": "Prywatny komentarz nie jest widoczny dla użytkownika, podczas gdy publiczny komentarz jest wyświetlany jako podpowiedź, które pojawia się, gdy użytkownik najedzie myszką nad elementem", - "custom_params": "Niestandardowe parametry", - "custom_params_hint": "Właściwa: --param=xy, błędna: --param xy", - "delete2": "Usuń wiadomości w miejscu docelowym, które nie znajdują się w źródle", "destination": "Miejsce docelowe", "disable_login": "Nie pozwalaj na logowanie(poczta przychodząca jest nadal akceptowana)", "domain_matches_hostname": "Domena pasuje do nazwy hosta", - "dry": "Symulacja synchronizacji", "gal": "Globalna lista adresów", "gal_info": "GAL zawiera wszystkie obiekty domeny i nie może być edytowany przez żadnego użytkownika. Wolne/zajęte logi w SOGo bedą nidostępne, jeśli są wyłączone! Uruchom ponownie SOGo, aby zastosować zmiany.", "generate": "generuj", @@ -105,11 +94,7 @@ "relayhost_wrapped_tls_info": "Proszę nie używać portów owiniętych TLS (głównie używanych na porcie 465).
\nUżyj dowolnego portu niezawiniętego i uruchom STARTTLS. Polityka TLS w celu egzekwowania TLS może być utworzona w \"mapie zasad TLS\".", "sieve_desc": "Krótki opis", "sieve_type": "Typ filtra", - "skipcrossduplicates": "Pomiń duplikaty wiadomości w folderach (na zasadzie kolejności zgłoszeń)", - "subscribeall": "Subskrybuj wszystkie foldery", "tags": "Tagi", - "timeout1": "Limit czasu połączenia z hostem zdalnym", - "timeout2": "Limit czasu połączenia dla lokalnego hosta", "validate": "Zatwierdź", "validation_success": "Zatwierdzone z powodzeniem" }, @@ -562,8 +547,6 @@ "active": "Aktywny", "alias": "Edytuj alias", "backup_mx_options": "Opcje backup MX", - "delete1": "Usuń ze źródła po zakończeniu", - "delete2duplicates": "Usuń duplikaty w miejscu docelowym", "description": "Opis", "domain": "Edytuj domenę", "domain_admin": "Edytuj administratora domeny", @@ -572,7 +555,6 @@ "dont_check_sender_acl": "Wyłącz sprawdzanie nadawcy dla domeny %s + aliasów domeny", "edit_alias_domain": "Edytuj alias domeny", "encryption": "Szyfrowanie", - "exclude": "Wyklucz obiekty (regex)", "full_name": "Pełna nazwa", "hostname": "Nazwa hosta", "inactive": "Nieaktywny", @@ -581,8 +563,6 @@ "max_aliases": "Maks. liczba aliasów", "max_mailboxes": "Maks. liczba skrzynek", "max_quota": "Maks. wielkość skrzynki (MiB)", - "maxage": "Maksymalny wiek wiadomości, liczony w dniach, które będą pobierane ze zdalnego
(0 = pomiń wiek>", - "mins_interval": "Zakres (min)", "multiple_bookings": "Wielokrotne rejestracje", "password": "Hasło", "password_repeat": "Potwierdź hasło(powtórz)", @@ -595,8 +575,6 @@ "resource": "Zasób", "save": "Zapisz zmiany", "sender_acl": "Zezwalaj na wysłanie jako", - "subfolder2": "Synchronizuj do podfolderu w skrzynce
(pusty = nie używaj subfolderu)", - "syncjob": "Edytuj polecenie synchronizacji", "target_address": "Adres/y Idź do (rozdzielone przecinkiem)", "target_domain": "Domena docelowa", "title": "Edytuj obiekt", @@ -612,14 +590,12 @@ "app_name": "Nazwa aplikacji", "app_passwd": "Hasło do aplikacji", "app_passwd_protocols": "Dozwolone protokoły dla hasła aplikacji", - "automap": "Spróbuj automatycznie mapować foldery (np. „Sent items”, „Sent” ⇒ „Sent” itp.)", "bcc_dest_format": "Miejscem docelowym BCC musi być jeden prawidłowy adres e-mail.
Jeśli chcesz wysłać kopię na wiele adresów, utwórz alias i użyj go tutaj.", "client_id": "Id klienta", "client_secret": "Tajny klucz klienta(sekret)", "comment_info": "Komentarz prywatny nie jest widoczny dla użytkownika, natomiast komentarz publiczny jest wyświetlany jako podpowiedź po najechaniu kursorem w widoku użytkownika", "created_on": "Stworzony na", "custom_attributes": "Niestandardowe atrybuty", - "delete2": "Usuń wiadomości na koncie docelowym, które nie występują na koncie źródłowym", "disable_login": "Zablokuj logowanie (przychodząca poczta nadal będzie przyjmowana)", "domain_footer": "Stopka dla całej domeny", "domain_footer_html": "Stopka HTML", @@ -663,7 +639,6 @@ "sender_acl_info": "Jeżeli użytkownik skrzynki pocztowej A ma pozwolenie na wysyłkę jako użytkownik skrzynki B, adres nadawcy nie jest automatycznie wyświetlany jako opcja w polu „Od” w SOGo.
\nUżytkownik skrzynki B musi utworzyć delegację w SOGo, aby użytkownik A mógł wybrać ich adres jako nadawcę. Aby zdelegować skrzynkę w SOGo, użyj menu (trzy kropki) po prawej stronie nazwy skrzynki w lewym górnym rogu, będąc w widoku poczty. To zachowanie nie dotyczy adresów aliasów.", "sieve_desc": "Krótki opis", "sieve_type": "Typ filtra", - "skipcrossduplicates": "Pomijaj duplikaty wiadomości w różnych folderach (pierwsza napotkana wiadomość zostaje zachowana).", "sogo_access": "Bezpośrednie przekazywanie do SOGo", "sogo_access_info": "Po zalogowaniu użytkownik jest automatycznie przekierowywany do SOGo.", "sogo_visible": "Alias jest widoczny w SOGO", @@ -672,13 +647,10 @@ "spam_filter": "Filtr spam", "spam_policy": "Dodaj lub usuń elementy z listy dozwolonych / zablokowanych", "spam_score": "Ustaw własny poziom punktacji spamu", - "timeout1": "Limit czasu połączenia z serwerem zdalnym", - "timeout2": "Limit czasu połączenia z serwerem lokalnym", "validate_save": "Zatwierdź i zapisz", "pushover_info": "Ustawienia powiadomień push będą miały zastosowanie do wszystkich czystych (niespamowych) wiadomości dostarczanych do %s w tym aliasów (współdzielonych, niewspółdzielonych, oznaczonych).", "mailbox_quota_def": "Domyślny limit skrzynki pocztowej", "mailbox_relayhost_info": "Dotyczy wyłącznie skrzynki pocztowej i bezpośrednich aliasów, nadpisuje ustawienie serwera pośredniczącego (relayhost) dla domeny.", - "maxbytespersecond": "Max. Ilość bajtów na sekundę
(0 = unlimited)", "mailbox_rename": "Zmień nazwę skrzynki pocztowej", "mailbox_rename_agree": "Stworzyłem kopię zapasową.", "mailbox_rename_warning": "WAŻNE! Utwórz kopię zapasową przed zmianą nazwy skrzynki pocztowej.", @@ -815,7 +787,6 @@ "remove": "Usuń", "resources": "Zasoby", "spam_aliases": "Alias tymczasowy", - "sync_jobs": "Polecenie synchronizacji", "target_address": "Adres Idź do", "target_domain": "Domena docelowa", "tls_enforce_in": "Uruchom TLS przychodzące", @@ -848,15 +819,6 @@ "sogo_visible_y": "Pokaż alias w SOGo", "stats": "Statystyki", "status": "Status", - "syncjob_check_log": "Sprawdź log", - "syncjob_last_run_result": "Wynik ostatniego uruchomienia", - "syncjob_EX_OK": "Sukces", - "syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem", - "syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania", - "syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło", "table_size": "Rozmiar tabeli", "table_size_show_n": "Pokaż %s elementy", "templates": "Szablony", @@ -1036,6 +998,37 @@ "u2f_deprecated": "Wygląda na to, że Twój klucz został zarejestrowany przy użyciu przestarzałej metody U2F. Dezaktywujemy dla Ciebie uwierzytelnianie dwuskładnikowe i usuniemy Twój klucz.", "u2f_deprecated_important": "Zarejestruj swój klucz w panelu administracyjnym za pomocą nowej metody WebAuthn." }, + "syncjobs": { + "sync_jobs": "Zadania synchronizacji", + "syncjob_check_log": "Sprawdź log", + "syncjob_last_run_result": "Wynik ostatniego uruchomienia", + "syncjob_EX_OK": "Sukces", + "syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem", + "syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania", + "syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło", + "add": "Dodaj polecenie synchronizacji", + "add_hint": "Pamiętaj, że hasła należy zapisywać w zwykłym tekście!", + "edit": "Edytuj polecenie synchronizacji", + "create": "Utwórz nowe polecenie synchronizacji", + "dry": "Symulacja synchronizacji", + "subfolder2": "Synchronizuj do podfolderu w skrzynce
(pusty = nie używaj subfolderu)", + "mins_interval": "Zakres pobierania (minuty)", + "maxage": "Maksymalny wiek wiadomości, liczony w dniach, które będą pobierane ze zdalnego
(0 = pomiń wiek>", + "exclude": "Wyklucz obiekty (regex)", + "automap": "Spróbuj automatycznie mapować foldery (\"Wysłane elementy\", \"Wysłane\" => \"Wysłane\" itp.)", + "delete1": "Usuń ze źródła po zakończeniu", + "delete2": "Usuń wiadomości w miejscu docelowym, które nie znajdują się w źródle", + "delete2duplicates": "Usuń duplikaty w miejscu docelowym", + "skipcrossduplicates": "Pomiń duplikaty wiadomości w folderach (na zasadzie kolejności zgłoszeń)", + "subscribeall": "Subskrybuj wszystkie foldery", + "timeout1": "Limit czasu połączenia z hostem zdalnym", + "timeout2": "Limit czasu połączenia dla lokalnego hosta", + "maxbytespersecond": "Max. Ilość bajtów na sekundę
(0 = unlimited)", + "custom_params": "Niestandardowe parametry" + }, "user": { "action": "Działanie", "active": "Aktywny", @@ -1052,7 +1045,6 @@ "aliases_send_as_all": "Nie sprawdzaj dostępu nadawcy dla następujących domen(y) i ich/jej aliasów", "change_password": "Zmień hasło", "client_configuration": "Instrukcja konfiguracji w programach pocztowych i na smartfonach", - "create_syncjob": "Utwórz nowe polecenie synchronizacji", "daily": "Co dzień", "day": "Dzień", "description": "Opis", @@ -1107,7 +1099,6 @@ "spamfilter_wl": "Biała lista", "spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby nigdy nie były klasyfikowane jako spam. Można używać symboli wieloznacznych (wildcardów).Filtr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej", "spamfilter_yellow": "Żółty: ta wiadomość może być spamem, zostanie oznaczona jako spam i przeniesiona do folderu spam", - "sync_jobs": "Zadania synchronizacji", "tag_handling": "Ustaw obsługę znaczników pocztowych", "tag_help_example": "Przykład adresu email z etykietą: ja+Facebook@example.org", "tag_help_explain": "W podfolderze: tworzy nowy podfolder z nazwą taką jak etykieta, który zostanie umieszczony pod Skrzynką odbiorczą (\"Skrzynka odbiorcza/Facebook\").
\r\nW temacie: nazwy etykiet zostaną dodane na początku tematów wiadomości, np.: \"[Facebook] Moje wiadomości\".", @@ -1188,15 +1179,6 @@ "sender_acl_disabled": "Sprawdzenie nadawcy jest wyłączone", "spam_score_reset": "Przywróć domyślne ustawienia serwera", "status": "Status", - "syncjob_check_log": "Sprawdź log", - "syncjob_last_run_result": "Wynik ostatniego uruchomienia", - "syncjob_EX_OK": "Sukces", - "syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem", - "syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania", - "syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło", "text": "Tekst", "tfa_info": "Uwierzytelnianie dwuskładnikowe pomaga chronić Twoje konto. Jeśli je włączysz, będziesz potrzebować haseł aplikacji, aby logować się do programów lub usług, które nie obsługują uwierzytelniania dwuskładnikowego (np. klientów poczty).", "title": "Tytuł", diff --git a/data/web/lang/lang.pt-br.json b/data/web/lang/lang.pt-br.json index 5645537c6..0e84e599e 100644 --- a/data/web/lang/lang.pt-br.json +++ b/data/web/lang/lang.pt-br.json @@ -44,15 +44,9 @@ "app_name": "Nome do aplicativo", "app_password": "Adicionar senha do aplicativo", "app_passwd_protocols": "Protocolos permitidos para a senha do aplicativo", - "automap": "Tente mapear pastas automaticamente (“Itens enviados”, “Enviados” => “Enviados” etc.)", "backup_mx_options": "Opções de relé", "bcc_dest_format": "O destino do BCC deve ser um único endereço de e-mail válido.
Se precisar enviar uma cópia para vários endereços, crie um alias e use-o aqui.", "comment_info": "Um comentário privado não é visível para o usuário, enquanto um comentário público é mostrado como dica de ferramenta ao passar o mouse sobre ele na visão geral do usuário", - "custom_params": "Parâmetros personalizados", - "custom_params_hint": "Certo: --param=xy, errado: --param xy", - "delete1": "Excluir da fonte quando concluído", - "delete2": "Excluir mensagens no destino que não estão na origem", - "delete2duplicates": "Excluir duplicatas no destino", "description": "Descrição", "destination": "Destino", "disable_login": "Não permitir login (e-mails recebidos ainda são aceitos)", @@ -60,7 +54,6 @@ "domain_matches_hostname": "O domínio %s corresponde ao nome do host", "domain_quota_m": "Cota total de domínio (MiB)", "enc_method": "Método de criptografia", - "exclude": "Excluir objetos (regex)", "full_name": "Nome completo", "gal": "Lista de endereços global", "gal_info": "A GAL contém todos os objetos de um domínio e não pode ser editada por nenhum usuário. Faltam informações de disponibilidade no SoGo, se desativadas! Reinicie o SoGo para aplicar as alterações.", @@ -76,7 +69,6 @@ "mailbox_username": "Nome de usuário (parte esquerda de um endereço de e-mail)", "max_aliases": "Máximo de aliases possíveis", "max_mailboxes": "Número máximo de mailboxes possíveis", - "mins_interval": "Intervalo de votação (minutos)", "multiple_bookings": "Várias reservas", "nexthop": "Próximo salto", "password": "Senha", @@ -96,20 +88,13 @@ "select_domain": "Selecione primeiro um domínio", "sieve_desc": "Breve descrição", "sieve_type": "Tipo de filtro", - "skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)", - "subscribeall": "Assine todas as pastas", - "syncjob": "Adicionar tarefa de sincronização", - "syncjob_hint": "Esteja ciente de que as senhas precisam ser salvas em texto simples!", "tags": "Etiquetas", "target_address": "Vá para endereços", "target_address_info": "Endereço (s) de e-mail completo (separado por vírgula).", "target_domain": "Domínio de destino", - "timeout1": "Tempo limite para conexão com o host remoto", - "timeout2": "Tempo limite para conexão com o host local", "username": "Nome de usuário", "validate": "Validar", "validation_success": "Validado com sucesso", - "dry": "Simular sincronização", "internal": "Interno", "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio.", "sender_allowed": "Permitir enviar como este alias" @@ -648,7 +633,6 @@ "app_name": "Nome do aplicativo", "app_passwd": "Senha do aplicativo", "app_passwd_protocols": "Protocolos permitidos para a senha do aplicativo", - "automap": "Tente mapear pastas automaticamente (“Itens enviados”, “Enviados” => “Enviados” etc.)", "backup_mx_options": "Opções de relé", "bcc_dest_format": "O destino do BCC deve ser um único endereço de e-mail válido.
Se precisar enviar uma cópia para vários endereços, crie um alias e use-o aqui.", "client_id": "ID Cliente", @@ -656,9 +640,6 @@ "comment_info": "Um comentário privado não é visível para o usuário, enquanto um comentário público é mostrado como dica de ferramenta ao passar o mouse sobre ele na visão geral do usuário", "created_on": "Criado em", "custom_attributes": "Atributos personalizados", - "delete1": "Excluir da fonte quando concluído", - "delete2": "Excluir mensagens no destino que não estão na origem", - "delete2duplicates": "Excluir duplicatas no destino", "delete_ays": "Confirme o processo de exclusão.", "description": "Descrição", "disable_login": "Não permitir login (e-mails recebidos ainda são aceitos)", @@ -682,7 +663,6 @@ "dont_check_sender_acl": "Desativar a verificação de remetente para o domínio %s (+ domínios de alias)", "edit_alias_domain": "Editar domínio Alias", "encryption": "Criptografia", - "exclude": "Excluir objetos (regex)", "extended_sender_acl": "Endereços de remetentes externos", "extended_sender_acl_info": "Uma chave de domínio DKIM deve ser importada, se disponível.
\r\n Lembre-se de adicionar esse servidor ao registro TXT SPF correspondente.
\r\n Sempre que um domínio ou domínio de alias é adicionado a esse servidor, que se sobrepõe a um endereço externo, o endereço externo é removido.
\r\n Use @domain .tld para permitir o envio como * @domain .tld.", "force_pw_update": "Forçar a atualização da senha no próximo login", @@ -704,10 +684,7 @@ "max_aliases": "Máximo de aliases", "max_mailboxes": "Número máximo de mailboxes possíveis", "max_quota": "Cota máxima por mailbox (MiB)", - "maxage": "Duração máxima das mensagens em dias que serão pesquisadas remotamente
(0 = ignorar a idade)", - "maxbytespersecond": "Máximo de bytes por segundo
(0 = ilimitado)", "mbox_rl_info": "Esse limite de taxa é aplicado ao nome de login do SASL e corresponde a qualquer endereço “de” usado pelo usuário conectado. Um limite de taxa de mailbox substitui um limite de taxa em todo o domínio.", - "mins_interval": "Intervalo (min)", "multiple_bookings": "Várias reservas", "none_inherit": "Nenhum/Herdar", "nexthop": "Próximo salto", @@ -748,7 +725,6 @@ "sender_acl_info": "Se o usuário A da mailbox tiver permissão para enviar como usuário B da mailbox, o endereço do remetente não será exibido automaticamente como campo “de” selecionável no SoGo.
\n O usuário B da mailbox precisa criar uma delegação no SoGo para permitir que o usuário A da mailbox selecione seu endereço como remetente. Para delegar uma mailbox no SoGo, use o menu (três pontos) à direita do nome da sua caixa de correio no canto superior esquerdo enquanto estiver na visualização de e-mail. Esse comportamento não se aplica a endereços de alias.", "sieve_desc": "Breve descrição", "sieve_type": "Tipo de filtro", - "skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)", "sogo_access": "Encaminhamento direto para o SOGo", "sogo_access_info": "Após o login, o usuário é automaticamente redirecionado para o SOGo.", "sogo_visible": "O alias é visível no SoGo", @@ -757,12 +733,8 @@ "spam_filter": "Filtro de spam", "spam_policy": "Adicionar ou remover itens da lista de permissões/bloqueio", "spam_score": "Defina uma pontuação de spam personalizada", - "subfolder2": "Sincronizar na subpasta no destino
(vazio = não usar subpasta)", - "syncjob": "Editar tarefa de sincronização", "target_address": "Ir para o(s) endereço(s) (separados por vírgula)", "target_domain": "Domínio de destino", - "timeout1": "Tempo limite para conexão com o host remoto", - "timeout2": "Tempo limite para conexão com o host local", "title": "Editar objeto", "unchanged_if_empty": "Se inalterado, deixe em branco", "username": "Nome de usuário", @@ -1002,16 +974,6 @@ "spam_aliases": "Apelido temporário", "stats": "Estatísticas", "status": "Status", - "sync_jobs": "Trabalhos de sincronização", - "syncjob_check_log": "Registro de verificação", - "syncjob_last_run_result": "Resultado da última corrida", - "syncjob_EX_OK": "Sucesso", - "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexão", - "syncjob_EXIT_TLS_FAILURE": "Problema com conexão criptografada", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticação", - "syncjob_EXIT_OVERQUOTA": "A mailbox de destino está acima da cota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Não é possível se conectar ao servidor remoto", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome de usuário ou senha incorretos", "table_size": "Tamanho da mesa", "table_size_show_n": "Mostrar itens %s", "target_address": "Vá para o endereço", @@ -1251,6 +1213,37 @@ "setup_required": "Sua conta exige autenticação de dois fatores. Configure um método de 2FA para continuar.", "cancel_setup": "Cancelar e sair" }, + "syncjobs": { + "sync_jobs": "Trabalhos de sincronização", + "syncjob_check_log": "Registro de verificação", + "syncjob_last_run_result": "Resultado da última corrida", + "syncjob_EX_OK": "Sucesso", + "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexão", + "syncjob_EXIT_TLS_FAILURE": "Problema com conexão criptografada", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticação", + "syncjob_EXIT_OVERQUOTA": "A mailbox de destino está acima da cota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Não é possível se conectar ao servidor remoto", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome de usuário ou senha incorretos", + "add": "Adicionar tarefa de sincronização", + "add_hint": "Esteja ciente de que as senhas precisam ser salvas em texto simples!", + "edit": "Editar tarefa de sincronização", + "create": "Criar um novo trabalho de sincronização", + "dry": "Simular sincronização", + "subfolder2": "Sincronizar na subpasta no destino
(vazio = não usar subpasta)", + "mins_interval": "Intervalo de votação (minutos)", + "maxage": "Duração máxima das mensagens em dias que serão pesquisadas remotamente
(0 = ignorar a idade)", + "exclude": "Excluir objetos (regex)", + "automap": "Tente mapear pastas automaticamente (“Itens enviados”, “Enviados” => “Enviados” etc.)", + "delete1": "Excluir da fonte quando concluído", + "delete2": "Excluir mensagens no destino que não estão na origem", + "delete2duplicates": "Excluir duplicatas no destino", + "skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)", + "subscribeall": "Assine todas as pastas", + "timeout1": "Tempo limite para conexão com o host remoto", + "timeout2": "Tempo limite para conexão com o host local", + "maxbytespersecond": "Máximo de bytes por segundo
(0 = ilimitado)", + "custom_params": "Parâmetros personalizados" + }, "user": { "action": "Ação", "active": "Ativo", @@ -1280,7 +1273,6 @@ "clear_recent_successful_connections": "Conexões bem-sucedidas e claras", "client_configuration": "Mostrar guias de configuração para clientes de e-mail e smartphones", "create_app_passwd": "Crie a senha do aplicativo", - "create_syncjob": "Criar um novo trabalho de sincronização", "created_on": "Criado em", "daily": "Diariamente", "day": "dia", @@ -1384,16 +1376,6 @@ "spamfilter_wl_desc": "Endereços de e-mail na lista de permissões são configurados para nunca serem classificados como spam. Coringas podem ser utilizados. O filtro é aplicado apenas a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases catch-all e a própria mailbox.", "spamfilter_yellow": "Amarelo: esta mensagem pode ser spam, será marcada como spam e movida para sua pasta de lixo eletrônico", "status": "Status", - "sync_jobs": "Trabalhos de sincronização", - "syncjob_check_log": "Registro de verificação", - "syncjob_last_run_result": "Resultado da última corrida", - "syncjob_EX_OK": "Sucesso", - "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexão", - "syncjob_EXIT_TLS_FAILURE": "Problema com conexão criptografada", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticação", - "syncjob_EXIT_OVERQUOTA": "A mailbox de destino está acima da cota", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Não é possível se conectar ao servidor remoto", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome de usuário ou senha incorretos", "tag_handling": "Definir o tratamento para e-mails marcados", "tag_help_example": "Exemplo de endereço de e-mail com tag: eu+Facebook@exemplo.org", "tag_help_explain": "Na subpasta: uma nova subpasta com o nome da tag será criada abaixo da CAIXA DE ENTRADA (“Caixa de entrada/Facebook”).
\r\nNo assunto: o nome das tags será anexado ao assunto do e-mail, por exemplo: “[Facebook] Minhas notícias”.", diff --git a/data/web/lang/lang.pt-pt.json b/data/web/lang/lang.pt-pt.json index 64f85dd35..a2f9ff56e 100644 --- a/data/web/lang/lang.pt-pt.json +++ b/data/web/lang/lang.pt-pt.json @@ -36,8 +36,6 @@ "public_comment": "Comentário público", "sieve_desc": "Breve descrição", "sieve_type": "Tipo de filtro", - "subscribeall": "Subscrever todas as pastas", - "syncjob": "Adicionar sincronização", "validate": "Validar", "validation_success": "Validado com sucesso", "app_passwd_protocols": "Protocolos permitidos para senha de aplicação" @@ -282,6 +280,10 @@ "mailbox_removed": "Conta %s removida com sucesso", "resource_modified": "A conta %s foi alterada com sucesso" }, + "syncjobs": { + "add": "Adicionar sincronização", + "subscribeall": "Subscrever todas as pastas" + }, "user": { "action": "Ação", "active": "Ativo", diff --git a/data/web/lang/lang.ro-ro.json b/data/web/lang/lang.ro-ro.json index 8b002fdf5..5d3aeb252 100644 --- a/data/web/lang/lang.ro-ro.json +++ b/data/web/lang/lang.ro-ro.json @@ -43,15 +43,9 @@ "app_name": "Nume aplicație", "app_password": "Adaugă parolă aplicație", "app_passwd_protocols": "Protocoale permise pentru parola aplicației", - "automap": "Încearcă maparea automată a folderelor (\"Obiecte trimise\", \"Trimise\" => \"Trimise\" etc.)", "backup_mx_options": "Opțiuni backup MX", "bcc_dest_format": "Destinația BCC trebuie să fie o singură adresă de email validă.", "comment_info": "Un comentariu privat nu este vizibil pentru utilizator, în timp ce un comentariu public este afișat ca un tooltip când se trece peste el într-o privire de ansamblu asupra utilizatorilor", - "custom_params": "Parametri personalizați", - "custom_params_hint": "Corect: --param=xy, greşit: --param xy", - "delete1": "Șterge de la sursă când ai terminat", - "delete2": "Șterge mesajele la destinație care nu sunt la sursă", - "delete2duplicates": "Șterge duplicate la destinație", "description": "Descriere", "destination": "Destinaţie", "disable_login": "Dezactivează autentificarea", @@ -59,7 +53,6 @@ "domain_matches_hostname": "Domeniul %s se potrivește cu numele de gazdă", "domain_quota_m": "Cota totală domeniu (MiB)", "enc_method": "Metodă de criptare", - "exclude": "Exclude obiectele (regex)", "full_name": "Nume complet", "gal": "Lista adreselor globale", "gal_info": "GAL conține toate obiectele unui domeniu și nu poate fi editat de niciun utilizator. Informația liber/ocupat din SOGo lipsește dacă este dezactivat! Reporniți SOGo pentru a aplica modificările.", @@ -75,7 +68,6 @@ "mailbox_username": "Numele de utilizator (partea stângă a unei adrese de email)", "max_aliases": "Număr maxim posibil de aliasuri", "max_mailboxes": "Număr maxim posibil de cutii poștale", - "mins_interval": "Interval de interogare (minute)", "multiple_bookings": "Rezervări multiple", "nexthop": "Următorul hop", "password": "Parolă", @@ -95,15 +87,9 @@ "select_domain": "Selectează mai întâi un domeniu", "sieve_desc": "Descriere scurtă", "sieve_type": "Tip filtru", - "skipcrossduplicates": "Sari peste mesajele duplicate din toate folderele (primul venit, primul servit)", - "subscribeall": "Abonare la toate dosarele", - "syncjob": "Adaugă o lucrare de sincronizare", - "syncjob_hint": "Reține că parolele trebuie să fie salvate în text simplu!", "target_address": "Adrese goto", "target_address_info": "Adresă/e de email completă/e (separate prin virgulă).", "target_domain": "Domeniu țintă", - "timeout1": "Timeout pentru conectarea la gazda la distanță", - "timeout2": "Timeout pentru conectarea la gazda locală", "username": "Nume de utilizator", "validate": "Validează", "validation_success": "Validat cu succes", @@ -520,16 +506,12 @@ "app_name": "Nume aplicație", "app_passwd": "Parolă apicație", "app_passwd_protocols": "Protocoale permise pentru parola aplicației", - "automap": "Încearcă maparea automată a folderelor (\"Obiecte trimise\", \"Trimise\" => \"Trimise\" etc.)", "backup_mx_options": "Opțiuni backup MX", "bcc_dest_format": "Destinația BCC trebuie să fie o singură adresă de email validă.
Dacă trebuie să trimiteți o copie la mai multe adrese, creați un alias și utilizați-l aici.", "client_id": "ID Client", "client_secret": "Secret client", "comment_info": "Un comentariu privat nu este vizibil pentru utilizator, în timp ce un comentariu public este afișat ca un tooltip când se trece peste el într-o privire de ansamblu asupra utilizatorilor", "created_on": "Creat în", - "delete1": "Șterge de la sursă când ai terminat", - "delete2": "Șterge mesajele la destinație care nu sunt la sursă", - "delete2duplicates": "Șterge duplicate la destinație", "delete_ays": "Confirmați procesul de ștergere.", "description": "Descriere", "disable_login": "Nu permiteți autentificarea", @@ -540,7 +522,6 @@ "dont_check_sender_acl": "Dezactivează verificarea expeditorului pentru domeniul %s (+ domenii alias)", "edit_alias_domain": "Editează domeniul alias", "encryption": "Criptare", - "exclude": "Exclude obiecte (regex)", "extended_sender_acl": "Adrese de expeditor externe", "extended_sender_acl_info": "O cheie de domeniu DKIM ar trebui importată, dacă este disponibilă.
\r\n Nu uitați să adăugați acest server la înregistrările corespunzătoare SPF TXT.
\r\n De fiecare dată când un domeniu sau un domeniu alias este adăugat pe server, care se suprapune cu o adresă externă, adresa externă este eliminată.
\r\n Utilizați @domain.tld pentru a permite trimiterea ca *@domain.tld.", "force_pw_update": "Forțează o actualizare a parolei la următoarea conectare", @@ -561,10 +542,7 @@ "max_aliases": "Număr maxim de aliasuri", "max_mailboxes": "Număr maxim de cutii poștale", "max_quota": "Cotă maximă pentru cutia poștală (MiB)", - "maxage": "Vârsta maximă, în zile, a mesajelor care vor fi interogate de la distanță
(0 = ignoră vârsta)", - "maxbytespersecond": "Max. bytes pe secundă
(0 = nelimitat)", "mbox_rl_info": "Această limitare de rată se aplică pe numele de conectare SASL, se potrivește cu orice adresă „de la” folosită de către utilizatorul autentificat. O limitare a ratei căsuței poștale înlocuiește limitarea ratei la nivel de domeniu.", - "mins_interval": "Interval (min)", "multiple_bookings": "Rezervări multiple", "none_inherit": "Nici una / Moștenește", "nexthop": "Următorul hop", @@ -603,7 +581,6 @@ "sender_acl_info": "Dacă utilizatorului A de căsuță poștală îi este permis să trimită ca utilizatorul B, adresa expeditorului nu este afișată automat ca fiind câmp selectabil \"de la\" în SOGo.
\r\n Utilizatorul căsuței poștale B trebuie să creeze o delegație în SOGo pentru a permite utilizatorul cutiei poștale A să selecteze adresa ca expeditor. Pentru a delega o cutie poștală în SOGo, utilizați meniul (trei puncte) din dreapta numelui căsuței poștale în stânga sus, în timp ce vă aflați în vizualizarea e-mailului. Acest comportament nu se aplică adreselor alias.", "sieve_desc": "Descriere scurtă", "sieve_type": "Tip filtru", - "skipcrossduplicates": "Sari peste mesajele duplicate din toate folderele (primul venit, primul servit)", "sogo_access": "Redirecționare directă către SOGo", "sogo_access_info": "După logare, utilizatorul este redirecționat automat către SOGo.", "sogo_visible": "Aliasul este vizibil în SOGo", @@ -612,12 +589,8 @@ "spam_filter": "Filtru spam", "spam_policy": "Adaugă sau elimină elemente în lista albă/neagră", "spam_score": "Stabilește un scor de spam personalizat", - "subfolder2": "Sincronizează în subfolderul destinației
(gol = nu se utilizează subfolderul)", - "syncjob": "Editează lucrarea de sincronizare", "target_address": "Adresă/e goto (separate prin virgulă)", "target_domain": "Domeniu țintă", - "timeout1": "Timeout pentru conectarea la gazda la distanță", - "timeout2": "Timeout pentru conectarea la gazda locală", "title": "Editează obiectul", "unchanged_if_empty": "Dacă rămâne neschimbat se lasă necompletat", "username": "Nume de utilizator", @@ -816,16 +789,6 @@ "spam_aliases": "Alias temporar", "stats": "Statistici", "status": "Stare", - "sync_jobs": "Lucrări de sincronizare", - "syncjob_check_log": "Verificați jurnalul", - "syncjob_last_run_result": "Rezultatul ultimei rulări", - "syncjob_EX_OK": "Succes", - "syncjob_EXIT_CONNECTION_FAILURE": "Problemă de conectare", - "syncjob_EXIT_TLS_FAILURE": "Problemă cu conexiunea criptată", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problemă de autentificare", - "syncjob_EXIT_OVERQUOTA": "Cutia poștală țintă depășește cota maximă", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nu se poate conecta la server", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nume de utilizator sau parolă greșite", "table_size": "Mărime tabel", "table_size_show_n": "Arată %s articole", "target_address": "Adresă goto", @@ -1027,6 +990,36 @@ "waiting_usb_register": "În așteptarea dispozitivului USB...

Introdu parola ta mai sus și confirmă înregistrarea ta WebAuthn atingând butonul de pe dispozitivul tău USB WebAuthn.", "yubi_otp": "Autentificare Yubico OTP" }, + "syncjobs": { + "sync_jobs": "Lucrări de sincronizare", + "syncjob_check_log": "Verificați jurnalul", + "syncjob_last_run_result": "Rezultatul ultimei rulări", + "syncjob_EX_OK": "Succes", + "syncjob_EXIT_CONNECTION_FAILURE": "Problemă de conectare", + "syncjob_EXIT_TLS_FAILURE": "Problemă cu conexiunea criptată", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problemă de autentificare", + "syncjob_EXIT_OVERQUOTA": "Cutia poștală țintă depășește cota maximă", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nu se poate conecta la server", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nume de utilizator sau parolă greșite", + "add": "Adaugă o lucrare de sincronizare", + "add_hint": "Reține că parolele trebuie să fie salvate în text simplu!", + "edit": "Editează lucrarea de sincronizare", + "create": "Crează o nouă lucrare de sincronizare", + "subfolder2": "Sincronizează în subfolderul destinației
(gol = nu se utilizează subfolderul)", + "mins_interval": "Interval de interogare (minute)", + "maxage": "Vârsta maximă, în zile, a mesajelor care vor fi interogate de la distanță
(0 = ignoră vârsta)", + "exclude": "Exclude obiectele (regex)", + "automap": "Încearcă maparea automată a folderelor (\"Obiecte trimise\", \"Trimise\" => \"Trimise\" etc.)", + "delete1": "Șterge de la sursă când ai terminat", + "delete2": "Șterge mesajele la destinație care nu sunt la sursă", + "delete2duplicates": "Șterge duplicate la destinație", + "skipcrossduplicates": "Sari peste mesajele duplicate din toate folderele (primul venit, primul servit)", + "subscribeall": "Abonare la toate dosarele", + "timeout1": "Timeout pentru conectarea la gazda la distanță", + "timeout2": "Timeout pentru conectarea la gazda locală", + "maxbytespersecond": "Max. bytes pe secundă
(0 = nelimitat)", + "custom_params": "Parametri personalizați" + }, "user": { "action": "Acțiune", "active": "Activ", @@ -1055,7 +1048,6 @@ "clear_recent_successful_connections": "Ștergeți conexiunile reușite văzute", "client_configuration": "Afișează ghidurile de configurare pentru clienții de email și smartphone-uri", "create_app_passwd": "Crează parola aplicației", - "create_syncjob": "Crează o nouă lucrare de sincronizare", "created_on": "Creat în", "daily": "Zilnic", "day": "zi", @@ -1156,16 +1148,6 @@ "spamfilter_wl_desc": "Adrese de email de pe lista albă care nu trebuie niciodată să fie clasificate ca spam. Se pot folosi carduri speciale. Un filtru se aplică numai în cazul aliasurilor directe (aliasuri cu o singură căsuță poștală țintă), cu excepția aliasurilor catch-all și a cutiei poștale în sine.", "spamfilter_yellow": "Galben: acest mesaj poate fi spam, va fi etichetat ca spam și va fi mutat în dosarul de junk", "status": "Stare", - "sync_jobs": "Lucrări de sincronizare", - "syncjob_check_log": "Verificați jurnalul", - "syncjob_last_run_result": "Rezultatul ultimei rulări", - "syncjob_EX_OK": "Succes", - "syncjob_EXIT_CONNECTION_FAILURE": "Problemă de conectare", - "syncjob_EXIT_TLS_FAILURE": "Problemă cu conexiunea criptată", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problemă de autentificare", - "syncjob_EXIT_OVERQUOTA": "Cutia poștală țintă depășește cota maximă", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nu se poate conecta la server", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nume de utilizator sau parolă greșite", "tag_handling": "Setează manevrarea pentru mailurile etichetate", "tag_help_example": "Exemplu pentru o adresă de email etichetată: me+Facebook@example.org", "tag_help_explain": "În subfolder: un nou subfolder numit după etichetă va fi creat sub INBOX (\"INBOX/Facebook\").
\nÎn subiect: numele etichetelor va fi prefixat la subiectul mailurilor, de exemplu: \"[Facebook] My News\".", diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index 58353fb5c..605e7c323 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -44,24 +44,16 @@ "app_name": "Название приложения", "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", "app_password": "Добавить пароль приложения", - "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Параметры резервного MX", "bcc_dest_format": "Место назначения BCC должно быть единственным действительным адресом электронной почты.
Если вам нужно отправить копию на несколько адресов - используйте псевдоним.", "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя.", - "custom_params": "Пользовательские параметры", - "custom_params_hint": "Верно: --param=xy, неверно: --param xy", - "delete1": "Удаление из источника после завершения", - "delete2": "Удаление писем по месту назначения, которые не находятся на исходном", - "delete2duplicates": "Удаление дубликатов по назначению", "description": "Описание", "destination": "Назначение", "disable_login": "Вход в систему запрещён", "domain": "Домен", "domain_matches_hostname": "Домен %s соответствует имени хоста", "domain_quota_m": "Квота домена (MiB)", - "dry": "Имитировать синхронизацию", "enc_method": "Метод шифрования", - "exclude": "Исключить объекты (regex)", "full_name": "Полное имя", "gal": "GAL - Глобальная адресная книга", "gal_info": "GAL содержит все объекты домена и не подлежит редактированию. Информация о занятости в SOGo будет отсутствовать для домена, если эта функция не будет включена! Требуется перезапустить SOGo, чтобы изменения применились.", @@ -77,7 +69,6 @@ "mailbox_username": "Имя пользователя (часть адреса электронной почты слева)", "max_aliases": "Максимум псевдонимов", "max_mailboxes": "Максимум почтовых ящиков", - "mins_interval": "Интервал (в минутах)", "multiple_bookings": "Несколько бронирований", "nexthop": "Следующий хост", "password": "Пароль", @@ -97,16 +88,10 @@ "select_domain": "Пожалуйста, сначала выберите домен", "sieve_desc": "Краткое описание", "sieve_type": "Тип фильтра", - "skipcrossduplicates": "Пропускать повторяющиеся сообщения в папках", - "subscribeall": "Подписаться на все папки и подпапки", - "syncjob": "Добавить задание синхронизации", - "syncjob_hint": "Пароли к вашему аккаунту будут сохранены на сервере в виде простого текста!", "tags": "Теги", "target_address": "Владельцы псевдонима", "target_address_info": "Адреса почтовых ящиков, разделенные запятыми.", "target_domain": "Целевой домен", - "timeout1": "Тайм-аут для подключения к удаленному хосту", - "timeout2": "Тайм-аут для подключения к локальному хосту", "username": "Имя пользователя", "validate": "Проверить", "validation_success": "Проверка прошла успешно", @@ -646,7 +631,6 @@ "app_name": "Название приложения", "app_passwd": "Пароль приложения", "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", - "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Параметры резервного копирования MX", "bcc_dest_format": "Назначением для правила BCC должен быть единственный действительный адрес электронной почты.", "client_id": "ID клиента", @@ -654,9 +638,6 @@ "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя", "created_on": "Дата создания", "custom_attributes": "Пользовательские атрибуты", - "delete1": "Удаление из источника после завершения", - "delete2": "Удаление писем по месту назначения, которые не находятся на исходном", - "delete2duplicates": "Удаление дубликатов по назначению", "delete_ays": "Пожалуйста, подтвердите удаление", "description": "Описание", "disable_login": "Вход в систему запрещен", @@ -680,7 +661,6 @@ "dont_check_sender_acl": "Отключить проверку отправителя для домена %s и псевдонимов домена", "edit_alias_domain": "Изменить псевдоним домена", "encryption": "Шифрование", - "exclude": "Исключить объекты (regex)", "extended_sender_acl": "Внешние адреса почты", "extended_sender_acl_info": "Для внешних доменов должен быть импортирован или сгенерирован доменный ключ DKIM с соответствующей записью TXT в домене, если внешний домен использует DMARC.
\r\n Не забудьте добавить этот сервер к соответствующей записи SPF TXT внешнего домена.
\r\n Добавление домена из списка внешних адресов в mailcow автоматически удалит соответствующие записи из внешних адресов пользователей.
\r\n Чтобы разрешить пользователю отправку от имени *@domain.tld, укажите @domain.tld.", "footer_exclude": "Исключить из нижнего колонтитула", @@ -707,10 +687,7 @@ "max_aliases": "Максимум псевдонимов", "max_mailboxes": "Максимум почтовых ящиков", "max_quota": "Максимальная квота почтового аккаунта (MiB)", - "maxage": "Макс. возраст писем в днях, для загрузки с сервера
(0 = игнорировать возраст)", - "maxbytespersecond": "Макс. байт в сек
(0 = без лимита)", "mbox_rl_info": "Этот лимит применяется к SASL логину пользователя и соответствует любому адресу отправителя, используемому зарегистрированным пользователем. Лимит скорости почтового аккаунта перекрывает лимит скорости для всего домена.", - "mins_interval": "Интервал (в минутах)", "multiple_bookings": "Несколько бронирований", "nexthop": "Следующий хост", "none_inherit": "Отсутствует / Наследуется", @@ -751,7 +728,6 @@ "sender_acl_info": "Учтите, что если пользователю почтового аккаунта A разрешено отправлять от имени пользователя B, то адрес пользователя B не отобразится автоматически в списке \"Отправитель\" при написании писем в SOGo.
\r\n Пользователь почтового аккаунта B должен создать делегирование в SOGo, чтобы пользователь почтового аккаунта A мог выбрать его адрес в качестве отправителя. Делегирование находится в меню (троеточие) справа от имени почтового аккаунта в окне почты SOGo. Это поведение не относится к псевдонимам.", "sieve_desc": "Краткое описание", "sieve_type": "Тип фильтра", - "skipcrossduplicates": "Пропускать повторяющиеся сообщения в папках", "sogo_access": "Прямая переадресация в SOGo", "sogo_access_info": "После входа в систему пользователь автоматически перенаправляется в SOGo.", "sogo_visible": "Отображать псевдоним в SOGo", @@ -760,12 +736,8 @@ "spam_filter": "Спам фильтр", "spam_policy": "Добавить или удалить элементы белого/черного списка", "spam_score": "Задать индивидуальное определение спама", - "subfolder2": "Синхронизировать в подпапку
(пусто = в корень)", - "syncjob": "Изменить задание синхронизации", "target_address": "Владельцы псевдонима, (разделённые запятыми)", "target_domain": "Целевой домен", - "timeout1": "Тайм-аут для подключения к удаленному хосту", - "timeout2": "Тайм-аут для подключения к локальному хосту", "title": "Изменение объекта", "unchanged_if_empty": "Если без изменений - оставьте пустым", "username": "Имя пользователя", @@ -999,16 +971,6 @@ "spam_aliases": "Временный псевдоним", "stats": "Статистика", "status": "Статус", - "sync_jobs": "Задания синхронизации", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", - "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", - "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", - "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", - "syncjob_EX_OK": "Успешно", - "syncjob_check_log": "Проверить журнал", - "syncjob_last_run_result": "Результат последнего запуска", "table_size": "Размер таблицы", "table_size_show_n": "Отображать %s полей", "target_address": "Владельцы псевдонима", @@ -1242,6 +1204,37 @@ "webauthn": "WebAuthn аутентификация", "yubi_otp": "Yubico OTP аутентификация" }, + "syncjobs": { + "sync_jobs": "Задания синхронизации", + "syncjob_check_log": "Проверить журнал", + "syncjob_last_run_result": "Результат", + "syncjob_EX_OK": "Успешно", + "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", + "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", + "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", + "add": "Добавить задание синхронизации", + "add_hint": "Пароли к вашему аккаунту будут сохранены на сервере в виде простого текста!", + "edit": "Изменить задание синхронизации", + "create": "Создание нового задания синхронизации", + "dry": "Имитировать синхронизацию", + "subfolder2": "Синхронизировать в подпапку
(пусто = в корень)", + "mins_interval": "Интервал (в минутах)", + "maxage": "Макс. возраст писем в днях, для загрузки с сервера
(0 = игнорировать возраст)", + "exclude": "Исключить объекты (regex)", + "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", + "delete1": "Удаление из источника после завершения", + "delete2": "Удаление писем по месту назначения, которые не находятся на исходном", + "delete2duplicates": "Удаление дубликатов по назначению", + "skipcrossduplicates": "Пропускать повторяющиеся сообщения в папках", + "subscribeall": "Подписаться на все папки и подпапки", + "timeout1": "Тайм-аут для подключения к удаленному хосту", + "timeout2": "Тайм-аут для подключения к локальному хосту", + "maxbytespersecond": "Макс. байт в сек
(0 = без лимита)", + "custom_params": "Пользовательские параметры" + }, "user": { "action": "Действия", "active": "Активный", @@ -1271,7 +1264,6 @@ "clear_recent_successful_connections": "Очистить историю успешных подключений", "client_configuration": "Показать руководство по настройке почтовых клиентов и смартфонов", "create_app_passwd": "Создать новый пароль", - "create_syncjob": "Создание нового задания синхронизации", "created_on": "Дата создания", "daily": "Раз в день", "day": "день", @@ -1375,16 +1367,6 @@ "spamfilter_wl_desc": "Белый список адресов электронной почты позволяет никогда не классифицировать все полученные письма с указанных адресов как спам. Можно добавлять маски адресов. Фильтр применяется только к личным псевдонимам (с одним получателем), исключая catch-all псевдонимы и сам почтовый аккаунт.", "spamfilter_yellow": "Жёлтый: эти письма могут быть спамом, будут доставлены в папку \"Спам\"", "status": "Статус", - "sync_jobs": "Задания синхронизации", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", - "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", - "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", - "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", - "syncjob_EX_OK": "Успешно", - "syncjob_check_log": "Проверить журнал", - "syncjob_last_run_result": "Результат", "tag_handling": "Обработка тегированной почты", "tag_help_example": "Пример тегированного адреса электронной почты: ich+Facebook@example.org", "tag_help_explain": "Переместить в подпапку: будет создана новая подпапка в INBOX с именем тега, например: \"INBOX/Facebook\".
\r\n Добавить к теме письма: имя тега будет добавлено к теме письма, например: \"[Facebook] My News\".", diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index c13a08907..361e8eddb 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -43,21 +43,14 @@ "app_name": "Ime aplikacije", "app_password": "Dodaj geslo aplikacije", "app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije", - "automap": "Poskusi samodejno preslikati mape (\"Poslani elementi\", \"Poslano\" => \"Poslano\" ipd.)", "backup_mx_options": "Možnosti posredovanja (relay)", "comment_info": "Zasebni komentarji niso vidni uporabnikom, javni komentarji pa so prikazani kot opis, ko se z miško postavimo nad uporabnika v pregledu", - "custom_params": "Parametri po meri", - "custom_params_hint": "Pravilno: --param=xy, napačno: --param xy", - "delete1": "Izbriši na viru, ko je končano", - "delete2": "Izbriši sporočila na cilju, ki niso na viru", - "delete2duplicates": "Izbriši dvojnike na cilju", "description": "Opis", "destination": "Cilj", "domain": "Domena", "domain_matches_hostname": "Domena %s se ujema z nazivom gostitelja (hostname)", "domain_quota_m": "Kvota za celotno domeno (MiB)", "enc_method": "Metoda kriptiranja", - "exclude": "Izključi objekte (regex)", "full_name": "Polno ime", "gal": "Globalni seznam stikov (GAL)", "generate": "generiraj", @@ -71,7 +64,6 @@ "mailbox_username": "Uporabniško ime (levi del e-poštnega naslova)", "max_aliases": "Največje število dovoljenih vzdevkov", "max_mailboxes": "Največje dovoljeno število poštnih predalov", - "mins_interval": "Interval preverjanja (minute)", "multiple_bookings": "Več rezervacij", "nexthop": "Naslednji korak", "password_repeat": "Potrditev gesla (ponovi)", @@ -88,14 +80,10 @@ "select_domain": "Prosim najprej izberite domeno", "sieve_desc": "Kratek opis", "sieve_type": "Vrsta filtra", - "skipcrossduplicates": "Preskoči podvojena sporočila po mapah (prvi pride, prvi melje)", - "subscribeall": "Prijavi vse mape", - "syncjob": "Dodaj sinhronizacijsko opravilo", "tags": "Oznake", "target_address": "Pojdi na naslov", "target_address_info": "Polni e-poštni naslov/i (ločeni z vejico).", "target_domain": "Ciljna domena", - "timeout1": "Časovna omejitev za povezavo do oddaljenega gostitelja", "username": "Uporabniško ime", "validate": "Potrditev", "validation_success": "Uspešno potrjeno", @@ -108,9 +96,6 @@ "password": "Geslo", "post_domain_add": "SOGo zabojnik\"sogo-mailcow\" mora biti ponovno zagnan po dodajanju nove domene!

Dodatno se mora preveriti DNS konfiguracija domene. Ko je DNS konfiguracija domene odobrena, ponovno zaženite \"acme-mailcow\" za samodejno generiranje certifikatov za novo domeno (autoconfig.<domain>, autodiscover.<domain>).
Ta korak je opcijski in se ponovno poskuša vsakih 24 ur.", "relay_transport_info": "
Info
Definirate lahko preslikave transportov za cilj po meri za to domeno. Če ni nastavljena, se ustvari MX poizvedba.", - "syncjob_hint": "Pozor! Gesla se morajo shraniti v golo besedilo!", - "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja", - "dry": "Simuliraj sinhronizacijo", "internal": "Notranje", "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen.", "sender_allowed": "Dovoli pošiljanje kot ta vzdevek" @@ -648,7 +633,6 @@ "active": "Aktivno", "allow_from_smtp": "Dovoli samo tem IP naslovom uporabo SMTP", "bcc_dest_format": "Ciljna stran za polje SKP (BCC) mora biti en veljaven e-poštni naslov.
Če morate kopijo poslati na več naslovov, ustvarite vzdevek in ga uporabite tukaj.", - "automap": "Poskusite samodejno preslikati mape (\"Poslani predmeti\", \"Poslano\" => \"Poslano\" itd.)", "admin": "Uredi skrbnika", "domain_footer_info_vars": { "custom": "{= foo =} - Če ima poštni predal atribut po meri \"foo\" z vrednostjo \"bar\", vrne \"bar\"", @@ -663,7 +647,6 @@ "domains": "Domene", "extended_sender_acl_info": "Uvoziti je treba ključ domene DKIM, če je na voljo.
\n Ne pozabite dodati tega strežnika v ustrezni zapis SPF TXT.
\n Kadar koli je temu strežniku dodana domena ali vzdevek domene, ki se prekriva z zunanjim naslovom, se zunanji naslov odstrani.
\n Uporabite @domain.tld, da omogočite pošiljanje kot *@domain.tld.", "lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (.*\\.google\\.com za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka)", - "maxbytespersecond": "Največ bajtov na sekundo
(0 = neomejeno)", "pushover_sender_array": "Upoštevaj samo sledeče e-poštne naslove pošiljateljev (ločeni z vejico)", "mbox_rl_info": "Ta omejitev se uporabi za prijavno ime SASL in se ujema z naslovom \"od\", ki ga uporablja prijavljeni uporabnik. Omejitev poštnega nabiralnika preglasi omejitev za celotno domeno.", "kind": "Tip", @@ -671,9 +654,6 @@ "comment_info": "Zasebni komentar ni viden uporabniku, javni komentar pa se prikaže kot opis orodja, ko nanj v pregledu uporabnika zadržite miško", "created_on": "Ustvarjeno", "custom_attributes": "Atributi po meri", - "delete1": "Izbriši iz vira, ko je končano", - "delete2": "Izbriši sporočila na cilju, ki niso na izvoru", - "delete2duplicates": "Izbriši dvojnike na cilju", "delete_ays": "Prosim potrdite proces izbrisa.", "description": "Opis", "disable_login": "Onemogoči prijavo (dohodna pošta je še vedno sprejeta)", @@ -700,7 +680,6 @@ "domain_footer_skip_replies": "Prezri nogo v odgovorih na e-poštna sporočila", "domain_quota": "Kvota domene", "edit_alias_domain": "Uredi vzdevek domene", - "exclude": "Izključi objekte (regex)", "extended_sender_acl": "Naslovi zunanjih pošiljateljev", "force_pw_update": "Obvezna zamenjava gesla ob naslednji prijavi", "force_pw_update_info": "Ta uporabnik se bo lahko prijavil samo v %s. Gesla aplikacij ostajajo v rabi.", @@ -719,8 +698,6 @@ "max_aliases": "Največje število vzdevkov", "max_mailboxes": "Največje možno število poštnih predalov", "max_quota": "Največja kvota na poštni predal (MiB)", - "maxage": "Najvišja starost sporočil v dnevih, ki bodo prebrana z oddaljenega strežnika
(0 = prezri starost)", - "mins_interval": "Interval (min)", "multiple_bookings": "Več rezervacij", "none_inherit": "Brez / podeduj", "nexthop": "Naslednji skok", @@ -756,7 +733,6 @@ "sender_acl": "Dovoli pošiljanje kot", "sender_acl_disabled": "Preverjanje pošiljatelja je onemogočeno", "sieve_type": "Vrsta filtra", - "skipcrossduplicates": "Preskoči podvojena sporočila med mapami (po principu \"kdor prej pride, prej melje\")", "sogo_access": "Neposredno posredovanje na SOGo", "sogo_access_info": "Po prijavi je uporabnik samodejno preusmerjen na SOGo.", "sogo_visible": "Vzdevek je viden v SOGo", @@ -765,12 +741,8 @@ "spam_filter": "Filter neželene pošte", "spam_policy": "Dodajanje ali odstranjevanje elementov na seznam dovoljenih/zavrnjenih", "spam_score": "Nastavite oceno neželene pošte po meri", - "subfolder2": "Sinhroniziraj v podmapo na cilju
(prazno = ne uporabi podmape)", - "syncjob": "Urejanje sinhronizacijskega opravila", "target_address": "Pojdi na naslov/e (ločeno z vejico)", "target_domain": "Ciljna domena", - "timeout1": "Časovna omejitev za povezavo z oddaljenim gostiteljem", - "timeout2": "Časovna omejitev za povezavo z lokalnim gostiteljem", "title": "Urejanje predmeta", "unchanged_if_empty": "Če ni spremenjeno, pustite prazno", "username": "Uporabniško ime", @@ -965,8 +937,6 @@ "last_run": "Zadnji zagon", "sieve_preset_2": "Vedno označi e-pošto določenega pošiljatelja kot videno", "target_address": "Pojdi na naslov", - "syncjob_EXIT_TLS_FAILURE": "Težava s šifrirano povezavo", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Ni mogoče vzpostaviti povezave z oddaljenim strežnikom", "tls_map_parameters_info": "Prazno ali parametri, na primer: protocols=!SSLv2 ciphers=medium exclude=3DES", "tls_policy_maps_enforced_tls": "Ti pravilniki bodo preglasili tudi vedenje uporabnikov poštnih predalov, ki vsiljujejo odhodne povezave TLS. Če spodaj ni pravilnika, bodo ti uporabniki uporabili privzete vrednosti, določene kot smtp_tls_mandatory_protocols in smtp_tls_mandatory_ciphers.", "spam_aliases": "Začasni vzdevek", @@ -982,13 +952,6 @@ "sogo_visible_y": "Prikaži vzdevek v SOGo", "stats": "Statistika", "status": "Stanje", - "sync_jobs": "Sinhronizacija opravil", - "syncjob_check_log": "Preveri dnevnik", - "syncjob_last_run_result": "Rezultat zadnjega zagona", - "syncjob_EX_OK": "Uspešno", - "syncjob_EXIT_CONNECTION_FAILURE": "Težava s povezavo", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Težava z overjanjem", - "syncjob_EXIT_OVERQUOTA": "Ciljni poštni predal presega kvoto", "table_size_show_n": "Prikaži %s elementov", "target_domain": "Ciljna domena", "template": "Predloga", @@ -999,7 +962,6 @@ "tls_map_parameters": "Parametri", "tls_map_policy": "Politika", "tls_policy_maps": "Zemljevidi pravilnikov TLS", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Napačno uporabniško ime ali geslo", "templates": "Predloge", "tls_policy_maps_long": "Preglasitve zemljevidov pravilnikov odhodnega TLS", "table_size": "Velikost tabele", @@ -1259,8 +1221,38 @@ "hour": "sporočil / uro", "day": "sporočil / dan" }, + "syncjobs": { + "sync_jobs": "Sinhronizacija opravil", + "syncjob_check_log": "Preveri dnevnik", + "syncjob_last_run_result": "Rezultat zadnjega zagona", + "syncjob_EX_OK": "Uspeh", + "syncjob_EXIT_CONNECTION_FAILURE": "Težava s povezavo", + "syncjob_EXIT_TLS_FAILURE": "Težava s šifrirano povezavo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Težava z overjanjem", + "syncjob_EXIT_OVERQUOTA": "Ciljni poštni predal presega kvoto", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Ni mogoče vzpostaviti povezave z oddaljenim strežnikom", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Napačno uporabniško ime ali geslo", + "add": "Dodaj sinhronizacijsko opravilo", + "add_hint": "Pozor! Gesla se morajo shraniti v golo besedilo!", + "edit": "Urejanje sinhronizacijskega opravila", + "create": "Ustvari novo sinhronizacijsko opravilo", + "dry": "Simuliraj sinhronizacijo", + "subfolder2": "Sinhroniziraj v podmapo na cilju
(prazno = ne uporabi podmape)", + "mins_interval": "Interval preverjanja (minute)", + "maxage": "Najvišja starost sporočil v dnevih, ki bodo prebrana z oddaljenega strežnika
(0 = prezri starost)", + "exclude": "Izključi objekte (regex)", + "automap": "Poskusi samodejno preslikati mape (\"Poslani elementi\", \"Poslano\" => \"Poslano\" ipd.)", + "delete1": "Izbriši na viru, ko je končano", + "delete2": "Izbriši sporočila na cilju, ki niso na viru", + "delete2duplicates": "Izbriši dvojnike na cilju", + "skipcrossduplicates": "Preskoči podvojena sporočila po mapah (prvi pride, prvi melje)", + "subscribeall": "Prijavi vse mape", + "timeout1": "Časovna omejitev za povezavo do oddaljenega gostitelja", + "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja", + "maxbytespersecond": "Največ bajtov na sekundo
(0 = neomejeno)", + "custom_params": "Parametri po meri" + }, "user": { - "create_syncjob": "Ustvari novo sinhronizacijsko opravilo", "open_webmail_sso": "Spletna pošta", "attribute": "Atribut", "description": "Opis", @@ -1398,15 +1390,6 @@ "spamfilter_wl": "Seznam dovoljenih", "spamfilter_yellow": "Rumena: to sporočilo je morda neželena pošta, označeno bo kot neželena pošta in premaknjeno v mapo z neželeno pošto", "status": "Stanje", - "sync_jobs": "Sinhronizacija opravil", - "syncjob_check_log": "Preveri dnevnik", - "syncjob_last_run_result": "Rezultat zadnjega zagona", - "syncjob_EXIT_CONNECTION_FAILURE": "Težava s povezavo", - "syncjob_EXIT_TLS_FAILURE": "Težava s šifrirano povezavo", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Težava z overjanjem", - "syncjob_EXIT_OVERQUOTA": "Ciljni poštni predal presega kvoto", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Ni mogoče vzpostaviti povezave z oddaljenim strežnikom", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Napačno uporabniško ime ali geslo", "tag_handling": "Nastavitev obravnave označene pošte", "tag_help_example": "Primer označenega e-poštnega naslova: jaz+Facebook@example.org", "tag_in_none": "Ne naredi ničesar", @@ -1428,7 +1411,6 @@ "years": "leta", "waiting": "Čakanje", "q_all": "Vse kategorije", - "syncjob_EX_OK": "Uspeh", "expire_never": "Nikoli ne poteče", "forever": "Za vedno", "spam_aliases_info": "Vzdevek za neželeno pošto je začasni e-poštni naslov, ki ga je mogoče uporabiti za zaščito pravih e-poštnih naslovov.
Po želji je mogoče nastaviti čas poteka veljavnosti, tako da se vzdevek po določenem obdobju samodejno deaktivira, s čimer se učinkovito znebite zlorabljenih ali razkritih naslovov.", diff --git a/data/web/lang/lang.sk-sk.json b/data/web/lang/lang.sk-sk.json index cf480cc70..aaac4ed99 100644 --- a/data/web/lang/lang.sk-sk.json +++ b/data/web/lang/lang.sk-sk.json @@ -43,15 +43,9 @@ "app_name": "Meno aplikácie", "app_passwd_protocols": "Povolené protokoly k heslu aplikácie", "app_password": "Pridať heslo aplikácie", - "automap": "Skúsiť automaticky mapovať priečinky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", "backup_mx_options": "Možnosti preposielania", "bcc_dest_format": "Cieľ kópie musí byť jedna platná emailová adresa. Pokiaľ potrebujete posielať kópie na viac adries, vytvorte Alias a použite ho tu.", "comment_info": "Súkromný komentár nie je viditeľný používateľovi, na rozdiel od verejného komentára, ktorý je prezentovaný ako popis v prehľade používateľov", - "custom_params": "Voliteľné parametre", - "custom_params_hint": "Správne: --param=xy, nesprávne: --param xy", - "delete1": "Vymazať zo zdrojovej schránky, po dokončení prenosu", - "delete2": "Vymazať správy v cieľovej schránke, ak nie sú v zdrojovej", - "delete2duplicates": "Vymazať duplikáty v cieľovej schránke", "description": "Popis", "destination": "Cieľ", "disable_login": "Zablokovať prihlásenie (nevzťahuje sa na prichádzajúcu poštu)", @@ -59,7 +53,6 @@ "domain_matches_hostname": "Doména %s sa zhoduje s hostiteľom", "domain_quota_m": "Celkové kvóta domény (MiB)", "enc_method": "Metóda zašifrovania", - "exclude": "Vylúčiť objekty (regex)", "full_name": "Celé meno", "gal": "Globálny zoznam adries", "gal_info": "Globálny zoznam adries obsahuje všetky objekty v doméne a nemôže byť upravená žiadnym používateľom. Informácie o Free/Busy stave budú chýbať ak bude vypnutý! Reštartujte SOGo na aplikáciu zmien.", @@ -75,7 +68,6 @@ "mailbox_username": "Prihlasovacie meno (ľavá časť emailovej adresy)", "max_aliases": "Max. počet aliasov", "max_mailboxes": "Max. počet mailových schránok", - "mins_interval": "Interval (min)", "multiple_bookings": "Viaceré rezervácie", "nexthop": "Ďalší skok", "password": "Heslo", @@ -95,20 +87,13 @@ "select_domain": "Najskôr vyberte doménu prosím", "sieve_desc": "Krátky popis", "sieve_type": "Typ filtru", - "skipcrossduplicates": "Preskočiť duplikované správy naprieč priečinkami (akceptuje sa prvý nález)", - "subscribeall": "Synchronizovať všetky priečinky", - "syncjob": "Pridať novú synchronizačnú úlohu", - "syncjob_hint": "Upozornenie, heslá budú uložené v obyčajnom texte!", "target_address": "Cieľové adresy", "target_address_info": "Celá emailová adresa/y (oddelené čiarkou).", "target_domain": "Cieľová doména", - "timeout1": "Časový limit pre spojenie so vzdialeným serverom", - "timeout2": "Časový limit pre spojenie s lokálnym serverom", "username": "Používateľské meno", "validate": "Overiť", "validation_success": "Úspešne overené", - "tags": "Štítky", - "dry": "Simulovať synchronizáciu" + "tags": "Štítky" }, "admin": { "access": "Prístup", @@ -538,16 +523,12 @@ "app_name": "Meno aplikácie", "app_passwd": "Heslo aplikácie", "app_passwd_protocols": "Povolené protokoly", - "automap": "Skúsiť automapovať priečinky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", "backup_mx_options": "Možnosti preposielania", "bcc_dest_format": "Cieľ kópie musí byť jedna platná emailová adresa. Pokiaľ potrebujete posielať kópie na viac adries, vytvorte Alias a použite ho tu.", "client_id": "ID klienta", "client_secret": "Klientský tajný kľúč", "comment_info": " Súkromný komentár nie je viditeľný používateľovi, na rozdiel od verejného komentára, ktorý je prezentovaný ako popis v prehľade používateľov", "created_on": "Vytvorené", - "delete1": "Vymazať zo zdrojovej schránky, po dokončení prenosu", - "delete2": "Vymazať správy v cieľovej schránke, ak nie sú v zdrojovej", - "delete2duplicates": "Vymazať duplikáty v cieľovej schránke", "delete_ays": "Potvrďte zmazanie.", "description": "Popis", "disable_login": "Zablokovať prihlásenie (nevzťahuje sa na prichádzajúcu poštu)", @@ -558,7 +539,6 @@ "dont_check_sender_acl": "Vypnúť kontrolu odosielateľa pre doménu %s (+ alias domény)", "edit_alias_domain": "Upraviť alias domény", "encryption": "Zašifrovanie", - "exclude": "Vyradiť objekty (regex)", "extended_sender_acl": "Externé adresy odosielateľa", "extended_sender_acl_info": "DKIM doménový kľúč by mal byť importovaný, ak je dostupný.
\r\n Nezabudnite pridať tento server prislúchajúcemu SPF TXT záznamu.
\r\n Kedykoľvek je doména alebo alias doména pridaná na server, ktorá sa zhoduje s externou adresou, táto externá adresa je odstránená.
\r\n Použite @domain.tld pre schopnosť poslať ako *@domain.tld.", "force_pw_update": "Vynútiť zmenu hesla pri ďalšom prihlásení", @@ -579,10 +559,7 @@ "max_aliases": "Max. počet aliasov", "max_mailboxes": "Max. počet schránok", "max_quota": "Max. kvóta pre mailovú schránku (MiB)", - "maxage": "Maximálne obdobie správ v dňoch, ktoré sa budú vzdialene dotazovať
(0 = ignorovať obdobie age)", - "maxbytespersecond": "Max. bajtov za sekundu
(0 = bez limitu)", "mbox_rl_info": "Tento limit je aplikovaný na báze prihlasovacieho mena SASL, je zhodný s \"from\" adresou používanou prihláseným používateľom. Prenosový limit u mailovej adresy má prednosť pred domain-wide prenosovým limitom.", - "mins_interval": "Interval (min)", "multiple_bookings": "Viaceré rezervácie", "nexthop": "Ďalší skok", "none_inherit": "Žiadne / Zdediť", @@ -621,7 +598,6 @@ "sender_acl_info": "Ak mailový používateľ A má povolenie poslať ako mailový používateľ B, adresa odosielateľa nieje automaticky viditeľná ako voliteľné \"from\" pole v SOGo.
\r\n Mailový používateľ B potrebuje vytvoriť delegáciu v SOGo, aby bol schopný mailový používateľ A vybrať jeho adresu ako odosielateľ. Na delegovanie mailovej adresy v SOGo, použite menu (tri bodky) napravo vášho mailového mena v hornom ľavom rohu, v prehľade správ. Toto neplatí pre alias adresy.", "sieve_desc": "Krátky popis", "sieve_type": "Typ filtru", - "skipcrossduplicates": "Preskočiť duplikované správy naprieč priečinkami (akceptuje sa prvý nález)", "sogo_access": "Priame presmerovanie na SOGo", "sogo_access_info": "Po prihlásení je používateľ automaticky presmerovaný na službu SOGo.", "sogo_visible": "Alias je viditeľný v SOGo", @@ -630,12 +606,8 @@ "spam_filter": "Spam filter", "spam_policy": "Pridať alebo odstrániť položky do bielej/čiernej listiny", "spam_score": "Stanoviť vlastnú spam hodnotu", - "subfolder2": "Synchronizovať do podadresára na cieli
(prázdne = nevytvárať podpriečinok)", - "syncjob": "Upraviť synchronizačnú úlohu", "target_address": "Cieľová adresa/y (oddelené čiarkou)", "target_domain": "Cieľová doména", - "timeout1": "Timeout pre spojenie s vzdialeným serverom", - "timeout2": "Timeout pre spojenie s lokálnym serverom", "title": "Upraviť objekt", "unchanged_if_empty": "Ak nemeníte, nechajte prázdne", "username": "Používateľské meno", @@ -849,16 +821,6 @@ "spam_aliases": "Dočasný alias", "stats": "Štatistika", "status": "Status", - "sync_jobs": "Synchronizačné úlohy", - "syncjob_check_log": "Skontrolujte záznam", - "syncjob_last_run_result": "Výsledok posledného spustenia", - "syncjob_EX_OK": "Úspech", - "syncjob_EXIT_CONNECTION_FAILURE": "Problém so spojením", - "syncjob_EXIT_TLS_FAILURE": "Problém so šifrovaným spojením", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikáciou", - "syncjob_EXIT_OVERQUOTA": "Cieľová schránka je plná", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nedá sa pripojiť k vzdialenému serveru", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživateľské meno alebo heslo", "table_size": "Veľkosť tabuľky", "table_size_show_n": "Zobraziť %s položiek", "target_address": "Cieľová adresa", @@ -1079,6 +1041,37 @@ "u2f_deprecated_important": "Zaregistrujte si svoj Kľúč v paneli správcu pomocou novej metódy WebAuthn.", "u2f_deprecated": "Zdá sa, že váš kľúč bol zaregistrovaný pomocou zastaranej metódy U2F. Deaktivujeme vám dvojfaktorovú autentifikáciu a odstránime váš Kľúč." }, + "syncjobs": { + "sync_jobs": "Synchronizačné úlohy", + "syncjob_check_log": "Skontrolujte záznam", + "syncjob_last_run_result": "Výsledok posledného spustenia", + "syncjob_EX_OK": "Úspech", + "syncjob_EXIT_CONNECTION_FAILURE": "Problém so spojením", + "syncjob_EXIT_TLS_FAILURE": "Problém so šifrovaným spojením", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikáciou", + "syncjob_EXIT_OVERQUOTA": "Cieľová schránka je plná", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nedá sa pripojiť k vzdialenému serveru", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživateľské meno alebo heslo", + "add": "Pridať novú synchronizačnú úlohu", + "add_hint": "Upozornenie, heslá budú uložené v obyčajnom texte!", + "edit": "Upraviť synchronizačnú úlohu", + "create": "Vytvoriť novú synchronizačnú úlohu", + "dry": "Simulovať synchronizáciu", + "subfolder2": "Synchronizovať do podadresára na cieli
(prázdne = nevytvárať podpriečinok)", + "mins_interval": "Interval (min)", + "maxage": "Maximálne obdobie správ v dňoch, ktoré sa budú vzdialene dotazovať
(0 = ignorovať obdobie age)", + "exclude": "Vylúčiť objekty (regex)", + "automap": "Skúsiť automaticky mapovať priečinky (\"Sent items\", \"Sent\" => \"Sent\" atd.)", + "delete1": "Vymazať zo zdrojovej schránky, po dokončení prenosu", + "delete2": "Vymazať správy v cieľovej schránke, ak nie sú v zdrojovej", + "delete2duplicates": "Vymazať duplikáty v cieľovej schránke", + "skipcrossduplicates": "Preskočiť duplikované správy naprieč priečinkami (akceptuje sa prvý nález)", + "subscribeall": "Synchronizovať všetky priečinky", + "timeout1": "Časový limit pre spojenie so vzdialeným serverom", + "timeout2": "Časový limit pre spojenie s lokálnym serverom", + "maxbytespersecond": "Max. bajtov za sekundu
(0 = bez limitu)", + "custom_params": "Voliteľné parametre" + }, "user": { "action": "Akcia", "active": "Aktívny", @@ -1107,7 +1100,6 @@ "clear_recent_successful_connections": "Vymazať nedávne úspešné prihlásenia", "client_configuration": "Zobraziť konfiguračné pokyny pre emailových klientov a smartfóny", "create_app_passwd": "Vytvoriť heslo aplikácie", - "create_syncjob": "Vytvoriť novú synchronizačnú úlohu", "created_on": "Vytvorené", "daily": "Denne", "day": "deň", @@ -1208,16 +1200,6 @@ "spamfilter_wl_desc": "Adresy na tomto zozname nebudú nikdy klasifikované ako spam. Môže byť použitý wildcard záznam (*). Filter je aplikovaný len na priame aliasy (aliasy s jednou cieľovou schránkou) s výnimkou doménových košov a samotnou schránkou.", "spamfilter_yellow": "Žltá: Táto správa môže byť spam, bude označená ako spam a presunutá do priečinku nevyžiadanej pošty", "status": "Status", - "sync_jobs": "Synchronizačné úlohy", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problém s autentifikáciou", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Chybné uživateľské meno alebo heslo", - "syncjob_EXIT_CONNECTION_FAILURE": "Problém so spojením", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nedá sa pripojiť k vzdialenému serveru", - "syncjob_EXIT_OVERQUOTA": "Cieľová schránka je plná", - "syncjob_EXIT_TLS_FAILURE": "Problém so šifrovaným spojením", - "syncjob_EX_OK": "Úspech", - "syncjob_check_log": "Skontrolujte záznam", - "syncjob_last_run_result": "Výsledok posledného spustenia", "tag_handling": "Zaobchádzanie s označenou poštou", "tag_help_example": "Príklad tagu e-mailovej adresy: me+Facebook@example.org", "tag_help_explain": "V podadresári: nový podadresár s menom tag-u bude vytvorený nižšie INBOX (\"INBOX/Facebook\").
\r\nIn subject: meno štítka bude pridané pred predmet pošty, napríklad: \"[Facebook] My News\".", diff --git a/data/web/lang/lang.sv-se.json b/data/web/lang/lang.sv-se.json index 56618938a..689401827 100644 --- a/data/web/lang/lang.sv-se.json +++ b/data/web/lang/lang.sv-se.json @@ -42,14 +42,8 @@ "alias_domain_info": "Endast giltiga domännamn (kommaavgränsade).", "app_name": "Applikationsnamn", "app_password": "Lägg till applikationslösenord", - "automap": "Försök att automatiskt mappa mappar (\"Skickade objekt\", \"Skickat\" => \"Skickat\" etc.)", "backup_mx_options": "Reläalternativ", "comment_info": "En privat kommentar är inte synlig för användaren, medan en offentlig kommentar visas som verktygstips när du hovrar den i en användares översikt", - "custom_params": "Egna parametrar", - "custom_params_hint": "Korrekt: --param=xy, Felaktigt: --param xy", - "delete1": "Ta bort från källan när du är klar", - "delete2": "Ta bort meddelanden på målet som inte finns på källan", - "delete2duplicates": "Ta bort dubbletter på destinationsservern", "description": "Beskrivning", "destination": "Destination", "disable_login": "Inaktivera inloggning (inkommande post kommer fortfarande tas emot)", @@ -57,7 +51,6 @@ "domain_matches_hostname": "Domänen %s kan inte matcha värdnamnet", "domain_quota_m": "Total kvot per postlåda (MiB)", "enc_method": "Krypteringsmetod", - "exclude": "Exkludera objekt (regex-filter)", "full_name": "Fullständiga namn", "gal": "Global adressbok", "gal_info": "Den global adressboken innehåller alla objekt i en domän och kan inte redigeras av någon användare. Informationen om tillgänglighet i SOGo är endast synlig när den globala adressboken är påslagen. Starta om SOGo för att tillämpa ändringar.", @@ -73,7 +66,6 @@ "mailbox_username": "Användarnamn (första delen av en e-postadress)", "max_aliases": "Max antal alias", "max_mailboxes": "Max antal postlådor", - "mins_interval": "Intervall (minuter)", "multiple_bookings": "Bokningsmöjligheter", "nexthop": "Reläserver", "password": "Lösenord", @@ -93,15 +85,9 @@ "select_domain": "Välj först en domän", "sieve_desc": "Kort beskrivning", "sieve_type": "Filtertyp", - "skipcrossduplicates": "Hoppa över duplicerade meddelanden i katalogerna (först till kvarn)", - "subscribeall": "Prenumerera på alla kataloger", - "syncjob": "Lägg till synkroniseringsjobb", - "syncjob_hint": "Observera att lösenord lagras i klartext!", "target_address": "Destinationsadresser", "target_address_info": "Fullständig e-postadress/adresser (separerade med komma).", "target_domain": "Måldomän", - "timeout1": "Timeout för anslutning till fjärrservern", - "timeout2": "Timeout för anslutning till lokalserver", "username": "Användarnamn", "validate": "Validera", "validation_success": "Korrekt validerad", @@ -487,16 +473,12 @@ "allowed_protocols": "Tillåtna protokoll", "app_name": "Applikationsnamn", "app_passwd": "Applikationslösenord", - "automap": "Koppla automatiskt de vanliga e-post katalogerna (\"Skickade meddelanden\", \"Skickat\" => \"Skickat\" mm)", "backup_mx_options": "Inställningar för vidarebefordran", "bcc_dest_format": "Destinationen för att skicka hemlig kopia måste vara en giltig e-postadress.", "client_id": "Klient-ID", "client_secret": "Klienthemlighet", "comment_info": "En privat kommentar är inte synlig för användaren, medan en offentlig kommentar visas i användaröversikten", "created_on": "Skapad vid", - "delete1": "Ta bort meddelande från källservern när överföringen är slutförd.", - "delete2": "Ta bort meddelanden från destinationsservern som inte finns på källservern", - "delete2duplicates": "Ta bort dubbletter på destinationsservern", "delete_ays": "Är du säker att du vill ta bort det här objektet?", "description": "Beskrivning", "disable_login": "Inaktivera inloggning (inkommande post kommer fortfarande tas emot)", @@ -507,7 +489,6 @@ "dont_check_sender_acl": "Inaktivera avsändarkontrollen för domänen %s (+alias-domäner)", "edit_alias_domain": "Ändra alias-domän", "encryption": "Kryptering", - "exclude": "Exkludera objekt (regex-filter)", "extended_sender_acl": "Externa avsändaradresser", "extended_sender_acl_info": "En DKIM-domännyckel bör importeras, om det är möjligt.
\r\n Kom ihåg att även lägga till motsvarande SPF TXT-uppslag på den här servern.
\r\n När en domän eller aliasdomän läggs till på den här servern som överlappar en extern adress, tas den externa adressen bort.
\r\n Använd @domain.tld för att kunna skicka som *@domain.tld.", "force_pw_update": "Kräv uppdatering av lösenordet vid nästa inloggning", @@ -526,10 +507,7 @@ "max_aliases": "Max antal alias", "max_mailboxes": "Max antal möjliga postlådor", "max_quota": "Max. kvot per postlåda (MiB)", - "maxage": "Hur många antal dagar bakåt som meddelanden ska hämtas
(0 = ignorera ålder)", - "maxbytespersecond": "Max antal bitar per sekund
(0 = obegränsat)", "mbox_rl_info": "Hastighetsgränsen tillämpas på SASL-inloggningsnamnet och påverkar därför alla avsändaradresser som den inloggade användaren använder. Gränsvärde på en enskild postlåda åsidosätter gränsvärdet för domänen.", - "mins_interval": "Intervall (min)", "multiple_bookings": "Flera bokningar", "nexthop": "Reläserver", "password": "Lösenord", @@ -566,19 +544,14 @@ "sender_acl_info": "Om användaren A tillåts skicka som användaren B visas inte avsändaradressen automatiskt i \"från\" fältet i SOGo.
\r\n Användaren B måste skapa en delegation i SOGo för att låta användaren A välja Användaren B's adress som avsändare. För att delegera en postlåda i SOGo, gå till menyn (tre punkter) till höger om ditt namn uppe till vänster i postvyn. Detta gäller inte för aliasadresser.", "sieve_desc": "Kort beskrivning", "sieve_type": "Filtertyp", - "skipcrossduplicates": "Hoppa över duplicerade meddelanden i katalogerna (först till kvarn)", "sogo_visible": "Alias visas i SOGo", "sogo_visible_info": "Det här alternativet påverkar endast objekt som kan visas i SOGo (delade eller icke-delade aliasadresser med destination för minst en lokal postlåda).", "spam_alias": "Skapa eller ändra tillfälliga e-postalias", "spam_filter": "Spamfilter", "spam_policy": "Lägg till eller ta bort objekt på vit- eller svartlista", "spam_score": "Ange ett anpassad skräppostvärde", - "subfolder2": "Synkronisera till en undermapp
(lämna tomt = använd inte undermapp)", - "syncjob": "Ändra synkroniseringsjobb", "target_address": "Vidarbefodringsadresser", "target_domain": "Måldomän", - "timeout1": "Timeout för anslutning till fjärrservern", - "timeout2": "Timeout för anslutning till lokalserver", "title": "Ändra objekt", "unchanged_if_empty": "Lämna blakt, om oförändrat", "username": "Användarnamn", @@ -752,7 +725,6 @@ "spam_aliases": "Tillfällig alias", "stats": "Statistik", "status": "Status", - "sync_jobs": "Synkroniseringsjobb", "table_size": "Tabellstorlek", "table_size_show_n": "Visa %s objekt", "target_address": "Vidarbefodringsadress", @@ -960,6 +932,27 @@ "fido2_success": "Enheten har registrerats", "fido2_validation_failed": "Verifiering misslyckades" }, + "syncjobs": { + "sync_jobs": "Synkroniseringsjobb", + "add": "Lägg till synkroniseringsjobb", + "add_hint": "Observera att lösenord lagras i klartext!", + "edit": "Ändra synkroniseringsjobb", + "create": "Skapa ett nytt synkroniseringsjobb", + "subfolder2": "Synkronisera till en undermapp
(lämna tomt = använd inte undermapp)", + "mins_interval": "Intervall (minuter)", + "maxage": "Hur många antal dagar bakåt som meddelanden ska hämtas
(0 = ignorera ålder)", + "exclude": "Exkludera objekt (regex-filter)", + "automap": "Försök att automatiskt mappa mappar (\"Skickade objekt\", \"Skickat\" => \"Skickat\" etc.)", + "delete1": "Ta bort från källan när du är klar", + "delete2": "Ta bort meddelanden på målet som inte finns på källan", + "delete2duplicates": "Ta bort dubbletter på destinationsservern", + "skipcrossduplicates": "Hoppa över duplicerade meddelanden i katalogerna (först till kvarn)", + "subscribeall": "Prenumerera på alla kataloger", + "timeout1": "Timeout för anslutning till fjärrservern", + "timeout2": "Timeout för anslutning till lokalserver", + "maxbytespersecond": "Max antal bitar per sekund
(0 = obegränsat)", + "custom_params": "Egna parametrar" + }, "user": { "action": "Åtgärd", "active": "Aktiv", @@ -984,7 +977,6 @@ "change_password": "Ändra lösenordet", "client_configuration": "Visa konfigurationsguider för e-postklienter och smartatelefoner", "create_app_passwd": "Skapa applikationslösenord", - "create_syncjob": "Skapa ett nytt synkroniseringsjobb", "created_on": "Skapad vid", "daily": "Dagligen", "day": "dag", @@ -1072,7 +1064,6 @@ "spamfilter_wl_desc": "Vitlistade e-postadresser kommer aldrig ska klassificeras som skräppost. Jokertecken kan användas. Ett filter kan bara används mot direkta aliasadresser (aliasadresser med en postlåda som destination) exklusive fånga-allt-aliasadresser och postlådan själv.", "spamfilter_yellow": "Gult: Det här medelandet kan vara spam, det kommer taggas som spam och flyttas till katalogen för skräppost", "status": "Status", - "sync_jobs": "Synkroniseringsjobb", "tag_handling": "Ange åtgärd för taggade medelanden", "tag_help_example": "Exempel på en taggad e-postadress: me+Facebook@example.org", "tag_help_explain": "I en undermapp: en ny undermapp namngiven efter taggen kommer skapas under INKORGEN (\"INKORGEN/Facebook\").
\r\nI ämnesraden: taggens namn kommer infogas i medelandets ämnesrad, exempelvis: \"[Facebook] Mina Nyheter\".", diff --git a/data/web/lang/lang.tr-tr.json b/data/web/lang/lang.tr-tr.json index bc1da77e7..4448887b8 100644 --- a/data/web/lang/lang.tr-tr.json +++ b/data/web/lang/lang.tr-tr.json @@ -44,15 +44,9 @@ "app_name": "Uygulama adı", "app_password": "Uygulama şifresi ekle", "app_passwd_protocols": "Uygulama şifresi için izin verilen protokoller", - "automap": "Klasörleri otomatik eşleştirmeyi deneyin (\"Gönderilen postalar\", \"Gönderilen\" => \"Gönderilen\" vb.)", "backup_mx_options": "Relay ayarları", "bcc_dest_format": "BCC hedefi tek bir geçerli e-posta adresi olmalıdır.
Bir kopyayı birden fazla adrese göndermeniz gerekiyorsa, bir takma ad oluşturun ve bunu burada kullanın.", "comment_info": "Özel bir yorum kullanıcı tarafından görülemezken, herkese açık bir yorum kullanıcının genel görünümünde üzerine gelindiğinde tooltip olarak gösterilir", - "custom_params": "Özel parametreler", - "custom_params_hint": "Doğru: --param=xy, yanlış: --param xy", - "delete1": "Tamamlandığında kaynaktan sil", - "delete2": "Eğer kaynakta yoksa hedefteki mesajları sil", - "delete2duplicates": "Hedefteki kopyaları sil", "description": "Açıklama", "destination": "Hedef", "disable_login": "Giriş yapmaya izin verme (gelen mailler yine de kabul edilir)", @@ -60,7 +54,6 @@ "domain_matches_hostname": "Alan adı %s ana bilgisayar adıyla eşleşiyor", "domain_quota_m": "Toplam alan adı kotası (MiB)", "enc_method": "Şifreleme yöntemi", - "exclude": "Hariç tutma kuralı (regex)", "full_name": "Tam isim", "gal": "Global Adres Listesi", "gal_info": "GAL bir alan alanının tüm nesnelerini içerir ve herhangi bir kullanıcı tarafından düzenlenemez. Eğer devre dışı bırakırsanız SOGo üzerindeki free/busy bilgileri kaybolur! Değişiklikleri uygulamak için SOGo'yu yeniden başlatın.", @@ -76,7 +69,6 @@ "mailbox_username": "Kullanıcı adı (e-posta adresinin sol kısmı)", "max_aliases": "Maksimum takma adı limiti", "max_mailboxes": "Maksimum e-posta hesabı", - "mins_interval": "Sorgulama döngüsü (dakika)", "multiple_bookings": "Birden fazla rezervasyon", "nexthop": "Sonraki atlama", "password": "Şifre", @@ -96,20 +88,13 @@ "select_domain": "Lütfen önce alan adı seçin", "sieve_desc": "Kısa açıklama", "sieve_type": "Filtre türü", - "skipcrossduplicates": "Klasörler arasında yinelenen(kopya) mesajları es geçin (ilk gelen mail baz alınır)", - "subscribeall": "Tüm klasörlere abone ol", - "syncjob": "Senkronizasyon görevi ekle", - "syncjob_hint": "Parolaların düz metin olarak kaydedilmesi gerektiğini unutmayın!", "tags": "Etiketler", "target_address": "Adreslere git", "target_address_info": "Tam e-posta adresleri (virgülle ayrılmış).", "target_domain": "Hedef alan adı", - "timeout1": "Uzak ana bilgisayara bağlantı için zaman aşımı", - "timeout2": "Yerel ana bilgisayara bağlantı için zaman aşımı", "username": "Kullanıcı Adı", "validate": "Doğrula", - "validation_success": "Doğrulama başarılı", - "dry": "Senkronizasyonu simüle et" + "validation_success": "Doğrulama başarılı" }, "admin": { "action": "İşlem", @@ -414,12 +399,9 @@ "sender_acl_info": "Posta kutusu A kullanıcısının posta kutusu kullanıcısı B olarak göndermesine izin veriliyorsa, gönderen adresi SOGo'da seçilebilir \\\"gönderen\\\" alanı olarak otomatik olarak görüntülenmez.
\\r\\n Posta kutusu B kullanıcısının izin vermesi için SOGo'da bir yetkilendirme oluşturması gerekir. posta kutusu kullanıcısı A, adresini gönderen olarak seçmek için. SOGo'da bir posta kutusuna yetki vermek için, posta görünümündeyken sol üstteki posta kutusu adınızın sağındaki menüyü (üç nokta) kullanın. Bu davranış, diğer ad adresleri için geçerli değildir.", "sogo_visible_info": "Bu seçenek yalnızca SOGo'da görüntülenebilen nesneleri etkiler (en az bir yerel posta kutusuna işaret eden paylaşılan veya paylaşılmayan diğer ad adresleri). Gizliyse, bir takma ad SOGo'da seçilebilir gönderici olarak görünmez.", "kind": "Nitelik", - "maxage": "Uzaktan sorgulanacak mesajların gün cinsinden maksimum ileti yaşı
(0 = yaşı yoksay)", - "maxbytespersecond": "Maks. saniye başına bayt
(0 = sınırsız)", "redirect_uri": "Yönlendirme URI'i", "relay_all": "Tüm alıcıları aktar", "domain_footer_skip_replies": "Yanıt postasındaki altbilgiyi yoksay", - "exclude": "Nesneleri hariç tut (normal ifade)", "extended_sender_acl": "Harici gönderen adresleri", "force_pw_update": "Bir sonraki girişte şifre güncellemesini zorla", "password": "Şifre", @@ -431,7 +413,6 @@ "pushover_info": "Anında iletme bildirimi ayarları, takma adlar (paylaşılan, paylaşılmayan, etiketli) dahil %s adresine teslim edilen tüm temiz (spam olmayan) postalara uygulanacaktır.", "pushover_sender_array": "Yalnızca aşağıdaki gönderen e-posta adreslerini göz önünde bulundurun (virgülle ayrılmış)", "scope": "Kapsam", - "skipcrossduplicates": "Klasörler arasında yinelenen mesajları atlayın (ilk gelen, ilk hizmet)", "sogo_visible": "Takma ad SOGo'da görünür", "relay_unknown_only": "Yalnızca var olmayan posta kutularını aktarın. Mevcut posta kutuları yerel olarak teslim edilecektir.", "relayhost": "Göndericiye bağlı aktarımlar", @@ -440,12 +421,8 @@ "save": "Değişiklikleri Kaydet", "spam_policy": "Beyaz/kara listeye öğe ekleyin veya kaldırın", "spam_score": "Özel bir spam puanı ayarlayın", - "subfolder2": "Hedefteki alt klasörle senkronize et
(boş = alt klasörü kullanmayın)", - "syncjob": "Senkronizasyon işini düzenle", "target_address": "Adrese git (virgülle ayrılmış)", "target_domain": "Hedef alan", - "timeout1": "Uzak ana bilgisayara bağlantı için zaman aşımı", - "timeout2": "Yerel ana bilgisayara bağlantı için zaman aşımı", "unchanged_if_empty": "Değişmediyse boş bırakın", "username": "Kullanıcı adı", "validate_save": "Doğrula ve kaydet", @@ -468,7 +445,6 @@ "delete_ays": "Lütfen silme işlemini onaylayın.", "description": "Açıklama", "pushover_only_x_prio": "Yalnızca yüksek öncelikli postayı dikkate alın [X-Priority: 1]", - "delete2duplicates": "Hedefteki kopyaları sil", "domain_footer": "Domain footer", "domain_footer_html": "HTML footer", "edit_alias_domain": "Takma ad alanını düzenle", @@ -486,14 +462,11 @@ "app_name": "Uygulama adı", "app_passwd": "Uygulama şifresi", "app_passwd_protocols": "Uygulama şifresi için izin verilen protokoller", - "automap": "Klasörleri otomatik eşlemeyi deneyin (\\\"Gönderilmiş öğeler\\\", \\\"Gönderildi\\\" => \\\"Gönderildi\\\" vb.)", "backup_mx_options": "Relay seçenekleri", "bcc_dest_format": "BCC, tek bir geçerli e-posta adresi olmalıdır.
Bir kopyayı birden fazla adrese göndermeniz gerekiyorsa, bir takma ad oluşturun ve burada kullanın.", "client_id": "Müşteri Kimliği", "client_secret": "Müşteri sırrı", "created_on": "Oluşturuldu", - "delete1": "Tamamlandığında kaynaktan silin", - "delete2": "Kaynakta olmayan hedefteki mesajları sil", "domain_footer_plain": "PLAIN footer", "multiple_bookings": "Çoklu rezervasyon", "none_inherit": "Yok / Miras", @@ -507,7 +480,6 @@ "grant_types": "Yetki türleri", "hostname": "Ana bilgisayar adı", "mbox_rl_info": "Bu hız sınırı SASL oturum açma adına uygulanır, oturum açmış kullanıcı tarafından kullanılan herhangi bir \\\"gönderen\\\" adresiyle eşleşir. Posta kutusu hız sınırı, alan genelindeki hız sınırını geçersiz kılar.", - "mins_interval": "Aralık(dakika)", "pushover_title": "Bildirim başlığı", "pushover_sound": "Ses", "pushover_vars": "Gönderici filtresi tanımlanmadığında, tüm postalar dikkate alınacaktır.
Regex filtreleri ve tam gönderen kontrolleri ayrı ayrı tanımlanabilir ve sıralı olarak değerlendirilecektir. Birbirlerine bağlı değildirler.
Metin ve başlık için kullanılabilir değişkenler (lütfen veri koruma politikalarını dikkate alın)", @@ -722,7 +694,6 @@ "tls_map_parameters_info": "Boş veya parametreler, örneğin: protokoller=!SSLv2 şifreleri=orta hariç tutma=3DES", "recipient_map": "Alıcı haritası", "sieve_info": "Kullanıcı başına birden fazla filtre saklayabilirsiniz, ancak aynı anda yalnızca bir ön filtre ve bir son filtre etkin olabilir.
\\r\\nHer filtre açıklanan sırayla işlenir. Ne başarısız bir komut dosyası ne de verilen bir \\\"keep;\\\", başka komut dosyalarının işlenmesini durdurmayacaktır. Global elek komut dosyalarında yapılan değişiklikler, Dovecot'un yeniden başlatılmasını tetikleyecektir.

Global elek ön filtresi • Ön filtre • Kullanıcı komut dosyaları • Son filtre • Küresel elek son filtresi", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Kimlik doğrulama sorunu", "tls_enforce_in": "TLS gelenini uygula", "tls_policy_maps_info": "Bu politika haritası, bir kullanıcının TLS politika ayarlarından bağımsız olarak giden TLS aktarım kurallarını geçersiz kılar.
\\r\\n Lütfen \\\"smtp_tls_policy_maps\\\" dokümanları.", "remove": "Kaldır", @@ -857,15 +828,6 @@ "sogo_visible": "Takma ad SOGo'da görünür", "sogo_visible_n": "SOGo'da takma adı gizle", "sogo_visible_y": "SOGo'da takma adı göster", - "sync_jobs": "İşleri senkronize et", - "syncjob_check_log": "Günlüğü kontrol et", - "syncjob_last_run_result": "Son çalıştırma sonucu", - "syncjob_EX_OK": "Başarılı", - "syncjob_EXIT_CONNECTION_FAILURE": "Bağlantı sorunu", - "syncjob_EXIT_TLS_FAILURE": "Şifreli bağlantıyla ilgili sorun", - "syncjob_EXIT_OVERQUOTA": "Hedef posta kutusu kotayı aştı", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Uzak sunucuya bağlanılamıyor", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Yanlış kullanıcı adı veya şifre", "table_size": "Tablo boyutu", "table_size_show_n": "%s öğeleri göster", "target_address": "Adrese git", @@ -1144,6 +1106,37 @@ "hour": "mesaj / saat", "day": "mesaj / gün" }, + "syncjobs": { + "sync_jobs": "İşleri senkronize et", + "syncjob_check_log": "İşleri senkronize et", + "syncjob_last_run_result": "Son çalıştırma sonucu", + "syncjob_EX_OK": "Başarılı", + "syncjob_EXIT_CONNECTION_FAILURE": "Bağlantı sorunu", + "syncjob_EXIT_TLS_FAILURE": "Şifreli bağlantı ile ilgili sorun", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Kimlik doğrulama sorunu", + "syncjob_EXIT_OVERQUOTA": "Hedef posta kutusu kotayı aştı", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Uzak sunucuya bağlanılamıyor", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Yanlış kullanıcı adı veya şifre", + "add": "Senkronizasyon görevi ekle", + "add_hint": "Parolaların düz metin olarak kaydedilmesi gerektiğini unutmayın!", + "edit": "Senkronizasyon işini düzenle", + "create": "Yeni senkronizasyon işi oluştur", + "dry": "Senkronizasyonu simüle et", + "subfolder2": "Hedefteki alt klasörle senkronize et
(boş = alt klasörü kullanmayın)", + "mins_interval": "Sorgulama döngüsü (dakika)", + "maxage": "Uzaktan sorgulanacak mesajların gün cinsinden maksimum ileti yaşı
(0 = yaşı yoksay)", + "exclude": "Hariç tutma kuralı (regex)", + "automap": "Klasörleri otomatik eşleştirmeyi deneyin (\"Gönderilen postalar\", \"Gönderilen\" => \"Gönderilen\" vb.)", + "delete1": "Tamamlandığında kaynaktan sil", + "delete2": "Eğer kaynakta yoksa hedefteki mesajları sil", + "delete2duplicates": "Hedefteki kopyaları sil", + "skipcrossduplicates": "Klasörler arasında yinelenen(kopya) mesajları es geçin (ilk gelen mail baz alınır)", + "subscribeall": "Tüm klasörlere abone ol", + "timeout1": "Uzak ana bilgisayara bağlantı için zaman aşımı", + "timeout2": "Yerel ana bilgisayara bağlantı için zaman aşımı", + "maxbytespersecond": "Maks. saniye başına bayt
(0 = sınırsız)", + "custom_params": "Özel parametreler" + }, "user": { "advanced_settings": "Gelişmiş ayarlar", "aliases_also_send_as": "Kullanıcı olarak göndermeye de izin verilir", @@ -1161,7 +1154,6 @@ "shared_aliases_desc": "Paylaşılan takma adlar, spam filtresi veya şifreleme politikası gibi kullanıcıya özel ayarlardan etkilenmez. Karşılık gelen spam filtreleri yalnızca bir yönetici tarafından etki alanı çapında bir politika olarak yapılabilir.", "action": "Eylem", "create_app_passwd": "Uygulama parolası oluştur", - "create_syncjob": "Yeni senkronizasyon işi oluştur", "change_password": "Şifre değiştirme", "clear_recent_successful_connections": "Görünen başarılı bağlantıları temizle", "client_configuration": "E-posta istemcileri ve akıllı telefonlar için yapılandırma kılavuzlarını göster", @@ -1268,7 +1260,6 @@ "spamfilter": "Spam filtresi", "spamfilter_behavior": "Derecelendirme", "spamfilter_bl": "Kara liste", - "sync_jobs": "İşleri senkronize et", "spamfilter_bl_desc": "Spam olarak sınıflandırılacak ve reddedilecek her zaman kara listeye alınan e-posta adresleri. Reddedilen postalar karantinaya kopyalanmaz. Joker karakterler kullanılabilir. Bir filtre yalnızca şunlara uygulanır tüm takma adları ve bir posta kutusunun kendisi hariç doğrudan takma adlar (tek bir hedef posta kutusuna sahip takma adlar).", "spamfilter_green": "Yeşil: bu mesaj spam değil", "spamfilter_hint": "İlk değer \\\"düşük spam puanını\\\" tanımlar, ikincisi \\\"yüksek spam puanını\\\" temsil eder.", @@ -1278,15 +1269,6 @@ "spamfilter_wl": "Beyaz liste", "spamfilter_wl_desc": "Beyaz listeye alınan e-posta adresleri spam olarak asla sınıflandırılmayacak şekilde programlanmıştır. Joker karakterler kullanılabilir. Bir filtre yalnızca, tümünü yakalama takma adları ve bir posta kutusunun kendisi.", "spamfilter_yellow": "Sarı: bu mesaj spam olabilir, spam olarak etiketlenecek ve gereksiz klasörünüze taşınacak", - "syncjob_check_log": "İşleri senkronize et", - "syncjob_last_run_result": "Son çalıştırma sonucu", - "syncjob_EX_OK": "Başarılı", - "syncjob_EXIT_CONNECTION_FAILURE": "Bağlantı sorunu", - "syncjob_EXIT_TLS_FAILURE": "Şifreli bağlantı ile ilgili sorun", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Kimlik doğrulama sorunu", - "syncjob_EXIT_OVERQUOTA": "Hedef posta kutusu kotayı aştı", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Uzak sunucuya bağlanılamıyor", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Yanlış kullanıcı adı veya şifre", "tag_handling": "Etiketli postalar için işlemeyi ayarla", "tag_help_example": "Etiketli bir e-posta adresi örneği: ben+Facebook@example.org", "tag_help_explain": "Alt klasörde: INBOX (\\\"INBOX/Facebook\\\") altında etiketten sonra adlandırılan yeni bir alt klasör oluşturulacak.
\\r\\nKonuda: etiketlerin adı posta konusunun önüne eklenecek, örnek: \\\"[Facebook] Haberlerim\\\".", diff --git a/data/web/lang/lang.uk-ua.json b/data/web/lang/lang.uk-ua.json index 61c401a07..4ab03f69e 100644 --- a/data/web/lang/lang.uk-ua.json +++ b/data/web/lang/lang.uk-ua.json @@ -36,12 +36,10 @@ "app_password": "Додати пароль додатка", "app_passwd_protocols": "Дозволені протоколи для пароля додатка", "comment_info": "Приватний коментар не видно користувачам, а публічний – відображається поряд із псевдонімом в особистому кабінеті користувача", - "custom_params": "Налаштування користувача", "gal": "GAL - Глобальна адресна книга", "mailbox_quota_m": "Максимальна квота поштового акаунту (MiB)", "max_aliases": "Максимум псевдонімів", "max_mailboxes": "Максимум поштових скриньок", - "mins_interval": "Інтервал (у хвилинах)", "multiple_bookings": "Декілька бронювань", "nexthop": "Наступний хост", "password": "Пароль", @@ -56,20 +54,15 @@ "select_domain": "Будь ласка, спочатку виберіть домен", "sieve_desc": "Короткий опис", "sieve_type": "Тип фільтра", - "skipcrossduplicates": "Пропускати в папках повідомлення, що повторюються", - "subscribeall": "Підписатися на всі папки та підпапки", - "syncjob": "Додати завдання синхронізації", "tags": "Мітки", "target_address": "Власники псевдоніму", "target_address_info": "Адреси поштових скриньок, розділені комами.", "target_domain": "Цільовий домен", - "timeout2": "Тайм-аут для підключення до локального хоста", "username": "Ім'я користувача", "validate": "Перевірити", "validation_success": "Перевірка пройшла успішно", "activate_filter_warn": "Активація цього фільтра відключить решту фільтрів цього типу.", "alias_address_info": "Вкажіть поштові адреси розділені комами або, якщо хочете пересилати всі повідомлення для домену власникам псевдоніму: @example.com. Дозволені тільки домени mailcow.", - "automap": "Автоматичне зіставлення папок (\"Надіслані листи\", \"Надіслані\" => \"Надіслані\" тощо)", "backup_mx_options": "Параметри резервного MX", "bcc_dest_format": "Призначення BCC має бути єдиною дійсною адресою електронної пошти.
Якщо вам потрібно надіслати копію на декілька адрес - використовуйте псевдонім.", "gal_info": "GAL містить усі об'єкти домену і не може бути відредагований. Інформація про зайнятість у SOGo буде відсутня для домену, якщо ця функція не буде увімкнена! Щоб зміни застосувалися, потрібно перезапустити SOGo.", @@ -80,8 +73,6 @@ "relay_all_info": "Якщо ви вирішите не ретранслювати всіх одержувачів, вам потрібно буде додати (\"сліпу\") поштову адресу для кожного одержувача, якого слід ретранслювати.", "relay_transport_info": "
Інфо
Ви можете налаштувати власний транспорт для домену. Якщо такої установки немає, то доставка буде виконана на основі MX-записів.", "relayhost_wrapped_tls_info": "Будь ласка, не використовуйте TLS порти (в основному це 465 порт).
\nВикористовуйте будь-який не порт TLS, який підтримує STARTTLS. А для захисту від downgrate атак - налаштуйте примусову політику TLS.", - "syncjob_hint": "Паролі до вашого акаунту будуть збережені на сервері у вигляді простого тексту!", - "timeout1": "Тайм-аут для підключення до віддаленого хоста", "active": "Активний", "add": "Додати", "add_domain_only": "Додати лише домен", @@ -89,15 +80,12 @@ "alias_address": "Псевдонім/и", "alias_domain": "Псевдонім домена", "alias_domain_info": "Лише дійсні доменні імена (розділені комами).", - "custom_params_hint": "Правильно: --param=xy, неправильно: --param xy", - "delete2": "Видалити повідомлення на місці призначення, які не є в джерелі", "description": "Опис", "destination": "Призначення", "disable_login": "Заборонити вхід (вхідна пошта все ще приймається)", "domain": "Домен", "domain_matches_hostname": "Домен %s відповідає імені хосту", "enc_method": "Метод шифрування", - "exclude": "Виключити об'єкти (regex)", "full_name": "Повне ім'я", "generate": "згенерувати", "goto_ham": "Запам'ятати як корисну пошту", @@ -106,10 +94,7 @@ "hostname": "Ім'я хоста", "inactive": "Неактивний", "kind": "Вид", - "delete1": "Видалити з джерела після завершення", - "delete2duplicates": "Видалити дублікати на місці призначення", - "domain_quota_m": "Загальна квота домену (МіБ)", - "dry": "Імітувати синхронізацію" + "domain_quota_m": "Загальна квота домену (МіБ)" }, "admin": { "access": "Налаштування доступу", @@ -547,14 +532,10 @@ "app_name": "Назва додатка", "app_passwd": "Пароль додатку", "app_passwd_protocols": "Дозволені протоколи для пароля додатку", - "automap": "Автоматичне зіставлення папок (\"Надіслані листи\", \"Надіслані\" => \"Надіслані\" тощо)", "backup_mx_options": "Параметри резервного MX", "client_id": "ID клієнта", "client_secret": "Секретний ключ користувача", "created_on": "Дата створення", - "delete1": "Видалити з джерела після завершення", - "delete2": "Видалити листи за місцем призначення, яких не має в джерелі", - "delete2duplicates": "Видалити дублікати за місцем призначення", "delete_ays": "Будь ласка, підтвердіть видалення.", "description": "Опис", "disable_login": "Заборонити вхід (вхідна пошта все ще приймається)", @@ -563,7 +544,6 @@ "domains": "Домени", "edit_alias_domain": "Редагувати псевдонім домену", "encryption": "Шифрування", - "exclude": "Виключити об'єкти (regex)", "extended_sender_acl": "Зовнішні адреси пошти", "force_pw_update": "Вимагати зміну пароля при наступному вході до системи", "force_pw_update_info": "Цей користувач зможе увійти лише в %s. Паролі додатків залишаються придатними для використання.", @@ -580,8 +560,6 @@ "max_aliases": "Максимум псевдонімів", "max_mailboxes": "Максимум поштових скриньок", "max_quota": "Максимальна квота поштового акаунту (МіБ)", - "maxbytespersecond": "Макс. байт у сек
(0 = без ліміту)", - "mins_interval": "Інтервал (у хвилинах)", "multiple_bookings": "Декілька бронювань", "none_inherit": "Відсутня / Успадковується", "nexthop": "Наступний хост", @@ -613,7 +591,6 @@ "sender_acl": "Дозволити надсилати листи від імені", "sieve_desc": "Короткий опис", "sieve_type": "Тип фільтра", - "skipcrossduplicates": "Пропускати в папках повідомлення, що повторюються", "sogo_access": "Пряме перенаправлення до SOGo", "sogo_access_info": "Після входу користувач автоматично перенаправляється на SOGo.", "sogo_visible": "Відображати псевдонім у SOGo", @@ -621,10 +598,7 @@ "spam_filter": "Спам фільтр", "spam_policy": "Додати або видалити елементи до білого/чорного списка", "spam_score": "Задати індивідуальне визначення спаму", - "subfolder2": "Синхронізувати в підпапку (порожньо = в корінь)", - "syncjob": "Редагувати завдання синхронізації", "target_domain": "Цільовий домен", - "timeout1": "Тайм-аут для підключення до віддаленого хоста", "title": "Редагувати об’єкт", "unchanged_if_empty": "Якщо не змінено, залиште порожнім", "username": "Ім'я користувача", @@ -641,7 +615,6 @@ "lookup_mx": "Призначення на основі резолвінгу MX-запису за регулярним виразом (.*\\.example\\.com$ для маршрутизації всієї пошти через цей хост, якщо MX закінчується на example.com)", "mailbox_quota_def": "Квота за замовчуванням", "mailbox_relayhost_info": "Застосовується лише до поштової скриньки та особистих псевдонімів, незалежно від налаштувань маршрутизації на рівні домену.", - "maxage": "Макс. вік листів у днях для завантаження з сервера
(0 = ігнорувати вік)", "mbox_rl_info": "Цей ліміт застосовується до SASL логіну користувача і відповідає будь-якій адресі відправника, який використовується зареєстрованим користувачем. Ліміт швидкості поштового облікового запису перекриває ліміт швидкості для всього домену.", "pushover_info": "Установки Push-повідомлення будуть застосовуватися до всієї пошти %s (за винятком спаму), включаючи псевдоніми (особисті, загальні та теговані).", "pushover_sender_array": "Отримувати повідомлення від списку адрес електронної пошти (розділені комами):", @@ -652,7 +625,6 @@ "sender_acl_info": "Врахуйте, що якщо користувачеві поштового облікового запису А дозволено відправляти від імені користувача Б, то адреса користувача Б не з'явиться автоматично у списку \"Відправник\" при написанні листів у SOGo.
\n Користувач поштового облікового запису Б повинен створити делегування в SOGo, щоб користувач поштового облікового запису А міг вибрати його адресу як відправника. Делегування знаходиться в меню (три крапки) праворуч від імені поштового облікового запису у вікні пошти SOGo. Ця поведінка не відноситься до псевдонімів.", "sogo_visible_info": "Впливає лише на об'єкти, які можуть відображатися в SOGo (персональні або загальні псевдоніми, що вказують щонайменше на один локальний поштовий обліковий запис). Зверніть увагу, що якщо функцію вимкнено, користувач не зможе вибрати адресу псевдоніма як відправника в SOGo.", "target_address": "Власники псевдоніма, (розділені комами)", - "timeout2": "Тайм-аут для підключення до локального хоста", "pushover_sound": "Звук", "domain_footer": "Нижній колонтитул домену", "domain_footer_info": "Нижні колонтитули для всього домену додаються до всіх вихідних електронних листів, пов’язаних з адресою в цьому домені.
Наступні змінні можна використовувати для нижнього колонтитула:", @@ -842,13 +814,6 @@ "spam_aliases": "Тимчасовий псевдонім", "stats": "Статистика", "status": "Статус", - "syncjob_check_log": "Перевірити журнал", - "syncjob_last_run_result": "Результат останнього запуску", - "syncjob_EX_OK": "Успішно", - "syncjob_EXIT_TLS_FAILURE": "Помилка встановлення шифрованого з'єднання", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Помилка авторизації", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не вдалося підключитися до віддаленого сервера", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильне ім'я користувача або пароль", "table_size": "Розмір таблиці", "table_size_show_n": "Показувати %s полів", "target_domain": "Цільовий домен", @@ -886,9 +851,6 @@ "sieve_info": "Ви можете зберегти декілька фільтрів для кожного користувача, але тільки один попередній фільтр та один постфільтр можуть бути активними одночасно.
\nКожен фільтр буде оброблено в описаному порядку. Ані зламаний скрипт, ані keep; не зупинить обробку подальших скриптів.

Global sieve prefilter • Prefilter • User scripts • Postfilter • Global sieve postfilter", "sieve_preset_8": "Відхилити повідомлення, надіслане на псевдонім, частиною якого є відправник", "sieve_preset_header": "Будь ласка, ознайомтесь із прикладами нижче. Для більш детальної інформації прочитайте Sieve Wikipedia.", - "sync_jobs": "Завдання синхронізації", - "syncjob_EXIT_CONNECTION_FAILURE": "Помилка зв'язку із сервером", - "syncjob_EXIT_OVERQUOTA": "Цільова поштова скринька перевищила квоту", "target_address": "Власники псевдоніму", "tls_map_dest_info": "Приклади: example.org, .example.org, [mail.example.org]:25", "tls_map_parameters_info": "Залиште поле порожнім або вкажіть параметри, наприклад: protocols=!SSLv2 ciphers=medium exclude=3DES", @@ -1103,6 +1065,37 @@ "waiting_usb_register": "Очікування USB-пристрою...

Будь ласка, введіть пароль вище та підтвердіть реєстрацію, натиснувши кнопку на USB пристрої.", "authenticators": "Аутентифікатори" }, + "syncjobs": { + "sync_jobs": "Завдання синхронізації", + "syncjob_check_log": "Перевірити журнал", + "syncjob_last_run_result": "Результат", + "syncjob_EX_OK": "Успішно", + "syncjob_EXIT_CONNECTION_FAILURE": "Помилка зв'язку із сервером", + "syncjob_EXIT_TLS_FAILURE": "Помилка встановлення шифрованого з'єднання", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблема аутентифікації", + "syncjob_EXIT_OVERQUOTA": "Цільова поштова скринька перевищила квоту", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не вдалося підключитися до віддаленого сервера", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильне ім'я користувача або пароль", + "add": "Додати завдання синхронізації", + "add_hint": "Паролі до вашого акаунту будуть збережені на сервері у вигляді простого тексту!", + "edit": "Редагувати завдання синхронізації", + "create": "Створити нове завдання синхронізації", + "dry": "Імітувати синхронізацію", + "subfolder2": "Синхронізувати в підпапку (порожньо = в корінь)", + "mins_interval": "Інтервал (у хвилинах)", + "maxage": "Макс. вік листів у днях для завантаження з сервера
(0 = ігнорувати вік)", + "exclude": "Виключити об'єкти (regex)", + "automap": "Автоматичне зіставлення папок (\"Надіслані листи\", \"Надіслані\" => \"Надіслані\" тощо)", + "delete1": "Видалити з джерела після завершення", + "delete2": "Видалити повідомлення на місці призначення, які не є в джерелі", + "delete2duplicates": "Видалити дублікати на місці призначення", + "skipcrossduplicates": "Пропускати в папках повідомлення, що повторюються", + "subscribeall": "Підписатися на всі папки та підпапки", + "timeout1": "Тайм-аут для підключення до віддаленого хоста", + "timeout2": "Тайм-аут для підключення до локального хоста", + "maxbytespersecond": "Макс. байт у сек
(0 = без ліміту)", + "custom_params": "Налаштування користувача" + }, "user": { "action": "Дії", "active": "Активний", @@ -1126,7 +1119,6 @@ "change_password_hint_app_passwords": "Ваш обліковий запис містить %d паролів додатків, які не змінюватимуться. Щоб керувати ними, перейдіть на вкладку \"Паролі додатків\".", "clear_recent_successful_connections": "Очистити історію успішних підключень", "create_app_passwd": "Створити новий пароль", - "create_syncjob": "Створити нове завдання синхронізації", "created_on": "Дата створення", "daily": "Раз на день", "day": "день", @@ -1205,14 +1197,6 @@ "spamfilter_wl": "Білий список", "spamfilter_yellow": "Жовтий: ці листи можуть бути спамом, будуть доставлені до папки \"Спам\"", "status": "Статус", - "sync_jobs": "Завдання синхронізації", - "syncjob_check_log": "Перевірити журнал", - "syncjob_last_run_result": "Результат", - "syncjob_EX_OK": "Успішно", - "syncjob_EXIT_CONNECTION_FAILURE": "Помилка зв'язку із сервером", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблема аутентифікації", - "syncjob_EXIT_OVERQUOTA": "Цільова поштова скринька перевищила квоту", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не вдалося підключитися до віддаленого сервера", "tag_handling": "Обробка тегованої пошти", "tag_help_example": "Приклад тегованої адреси електронної пошти: me+Facebook@example.org", "tag_in_none": "Нічого не робити", @@ -1258,8 +1242,6 @@ "spamfilter_behavior": "Фільтрування спаму", "spamfilter_bl_desc": "Всі листи, отримані від адрес електронної пошти, доданих до чорного списку, будуть відхилені та не будуть скопійовані до карантину. Використання * дозволено. Фільтр застосовується тільки до особистих псевдонімів (з одним одержувачем), за винятком catch-all псевдонімів та самого поштового облікового запису.", "spamfilter_wl_desc": "Адреси електронної пошти з білого списку запрограмовані так, щоб ніколи не класифікуватися як спам. Можна використовувати символи підстановки (*). Фільтр застосовується лише до прямих псевдонімів (псевдонімів з однією цільовою поштовою скринькою), за винятком псевдонімів загального доступу та самої поштової скриньки.", - "syncjob_EXIT_TLS_FAILURE": "Помилка встановлення шифрованого з'єднання", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильне ім'я користувача або пароль", "tag_help_explain": "Перемістити до підпапки: буде створено нову підпапку в INBOX з ім'ям тега, наприклад: \"INBOX/Facebook\".
\n Додати до теми листа: ім'я тега буде додано до теми листа, наприклад: \"[Facebook] My News\".", "tls_policy_warning": "Попередження: якщо ви увімкнете примусове шифрування пошти, ви можете зіткнутися з втратою листів.
Повідомлення, які не відповідають політиці, будуть відкидатися з повідомленням поштовим сервером про серйозний збій.
Цей параметр застосовується до вашої основної адреси електронної пошти (логіну), усім особистим псевдонімам та псевдонімам доменів. Маються на увазі лише псевдоніми з однією поштовою скринькою, як одержувач.", "year": "рік", diff --git a/data/web/lang/lang.uz-uz.json b/data/web/lang/lang.uz-uz.json index a8d2ecf82..a46c8c739 100644 --- a/data/web/lang/lang.uz-uz.json +++ b/data/web/lang/lang.uz-uz.json @@ -44,24 +44,16 @@ "app_name": "Ilova nomi", "app_password": "Ilova paroli qoʻshish", "app_passwd_protocols": "Ilova paroli uchun ruxsat etilgan protokollar", - "automap": "Papkalarni avtomatik xaritalashga harakat qilish (\"Sent items\", \"Sent\" => \"Sent\" va h.k.)", "backup_mx_options": "Relay sozlamalari", "bcc_dest_format": "BCC manzili bitta haqiqiy e-pochta manzili boʻlishi kerak.
Agar bir nechta manzilga nusxa joʻnatish kerak boʻlsa, taxallus yarating va shu yerda ishlating.", "comment_info": "Maxfiy izoh foydalanuvchiga koʻrinmaydi, ommaviy izoh esa foydalanuvchi koʻrinishida ustiga sichqoncha kelganda maslahat sifatida koʻrsatiladi", - "custom_params": "Maxsus parametrlar", - "custom_params_hint": "Toʻgʻri: --param=xy, notoʻgʻri: --param xy", - "delete1": "Tugatilgach manbadan oʻchirish", - "delete2": "Manbada boʻlmagan xabarlarni manzildan oʻchirish", - "delete2duplicates": "Manzildagi dublikatlarni oʻchirish", "description": "Tavsif", "destination": "Manzil", "disable_login": "Kirishni taqiqlash (kiruvchi pochta hali ham qabul qilinadi)", "domain": "Domen", "domain_matches_hostname": "%s domeni xost nomiga mos keladi", "domain_quota_m": "Domenning umumiy kvotasi (MiB)", - "dry": "Sinxronizatsiyani simulyatsiya qilish", "enc_method": "Shifrlash usuli", - "exclude": "Obyektlarni istisno qilish (regex)", "full_name": "Toʻliq ism", "gal": "Global manzillar roʻyxati", "gal_info": "GAL domenning barcha obyektlarini oʻz ichiga oladi va hech bir foydalanuvchi tomonidan tahrirlanmaydi. Oʻchirilsa, SOGodagi bandlik maʼlumotlari yoʻqoladi! Oʻzgarishlarni qoʻllash uchun SOGoni qayta ishga tushiring.", @@ -80,7 +72,6 @@ "mailbox_username": "Foydalanuvchi nomi (e-pochta manzilining chap qismi)", "max_aliases": "Mumkin boʻlgan maks. taxalluslar", "max_mailboxes": "Mumkin boʻlgan maks. pochta qutilari", - "mins_interval": "Soʻrov oraligʻi (daqiqa)", "multiple_bookings": "Bir nechta bandlovlar", "nexthop": "Keyingi nuqta", "password": "Parol", @@ -100,16 +91,10 @@ "select_domain": "Avval domenni tanlang", "sieve_desc": "Qisqa tavsif", "sieve_type": "Filtr turi", - "skipcrossduplicates": "Papkalararo dublikat xabarlarni oʻtkazib yuborish (birinchi kelgan, birinchi xizmatlanadi)", - "subscribeall": "Barcha papkalarga obuna boʻlish", - "syncjob": "Sinxronizatsiya vazifasi qoʻshish", - "syncjob_hint": "Parollar oddiy matn sifatida saqlanishini unutmang!", "tags": "Teglar", "target_address": "Yoʻnaltirish manzillari", "target_address_info": "Toʻliq e-pochta manzil(lar)i (vergul bilan ajratilgan).", "target_domain": "Maqsad domen", - "timeout1": "Masofaviy xostga ulanish vaqtining tugashi", - "timeout2": "Mahalliy xostga ulanish vaqtining tugashi", "username": "Foydalanuvchi nomi", "validate": "Tekshirish", "validation_success": "Muvaffaqiyatli tekshirildi" @@ -648,7 +633,6 @@ "app_name": "Ilova nomi", "app_passwd": "Ilova paroli", "app_passwd_protocols": "Ilova paroli uchun ruxsat etilgan protokollar", - "automap": "Papkalarni avtomatik xaritalashga harakat qilish (\"Sent items\", \"Sent\" => \"Sent\" va h.k.)", "backup_mx_options": "Relay sozlamalari", "bcc_dest_format": "BCC manzili bitta haqiqiy e-pochta manzili boʻlishi kerak.
Agar bir nechta manzilga nusxa joʻnatish kerak boʻlsa, taxallus yarating va shu yerda ishlating.", "client_id": "Mijoz ID", @@ -656,9 +640,6 @@ "comment_info": "Maxfiy izoh foydalanuvchiga koʻrinmaydi, ommaviy izoh esa foydalanuvchi koʻrinishida ustiga sichqoncha kelganda maslahat sifatida koʻrsatiladi", "created_on": "Yaratilgan sana", "custom_attributes": "Maxsus atributlar", - "delete1": "Tugatilgach manbadan oʻchirish", - "delete2": "Manbada boʻlmagan xabarlarni manzildan oʻchirish", - "delete2duplicates": "Manzildagi dublikatlarni oʻchirish", "delete_ays": "Iltimos, oʻchirish jarayonini tasdiqlang.", "description": "Tavsif", "disable_login": "Kirishni taqiqlash (kiruvchi pochta hali ham qabul qilinadi)", @@ -682,7 +663,6 @@ "dont_check_sender_acl": "%s domeni uchun joʻnatuvchi tekshiruvini oʻchirish (+ taxallus domenlari)", "edit_alias_domain": "Taxallus domenini tahrirlash", "encryption": "Shifrlash", - "exclude": "Obyektlarni istisno qilish (regex)", "extended_sender_acl": "Tashqi joʻnatuvchi manzillari", "extended_sender_acl_info": "DKIM domen kaliti mavjud boʻlsa, import qilinishi kerak.
\r\n Ushbu serverni mos SPF TXT yozuviga qoʻshishni unutmang.
\r\n Ushbu serverga tashqi manzil bilan ust-ust tushadigan domen yoki taxallus domeni qoʻshilganda, tashqi manzil olib tashlanadi.
\r\n *@domain.tld sifatida joʻnatishga ruxsat berish uchun @domain.tld dan foydalaning.", "force_pw_update": "Keyingi kirishda parolni majburiy yangilash", @@ -713,10 +693,7 @@ "max_aliases": "Maks. taxalluslar", "max_mailboxes": "Mumkin boʻlgan maks. pochta qutilari", "max_quota": "Bitta pochta qutisining maks. kvotasi (MiB)", - "maxage": "Masofadan soʻraladigan xabarlarning maksimal yoshi (kunlarda)
(0 = yoshni e'tiborsiz qoldirish)", - "maxbytespersecond": "Maks. soniyaga bayt
(0 = cheksiz)", "mbox_rl_info": "Bu tezlik chegarasi SASL kirish nomiga qoʻllaniladi, u kirgan foydalanuvchi tomonidan ishlatiladigan har qanday \"from\" manziliga mos keladi. Pochta qutisi tezlik chegarasi domen boʻyicha tezlik chegarasini bekor qiladi.", - "mins_interval": "Oraliq (daqiqa)", "mta_sts": "MTA-STS", "mta_sts_info": "MTA-STS — pochta serverlari oʻrtasida xat yetkazib berishni haqiqiy sertifikatli TLS dan foydalanishga majburlovchi standart.
U DNSSEC mavjud emasligi yoki qoʻllab-quvvatlanmasligi sababli DANE imkoni boʻlmaganda ishlatiladi.
Eslatma: Agar qabul qiluvchi domen DNSSEC bilan DANE ni qoʻllab-quvvatlasa, DANE har doim afzal koʻriladi — MTA-STS faqat zaxira sifatida ishlaydi.", "mta_sts_version": "Versiya", @@ -768,7 +745,6 @@ "sender_acl_info": "Agar A pochta qutisi foydalanuvchisi B pochta qutisi foydalanuvchisi sifatida joʻnatishga ruxsat etilgan boʻlsa, joʻnatuvchi manzili SOGo da tanlanadigan \"from\" maydoni sifatida avtomatik koʻrsatilmaydi.
\r\n B pochta qutisi foydalanuvchisi A pochta qutisi foydalanuvchisi oʻz manzilini joʻnatuvchi sifatida tanlay olishi uchun SOGo da delegatsiya yaratishi kerak. SOGo da pochta qutisini delegatsiya qilish uchun pochta koʻrinishida yuqori chap qismdagi pochta qutisi nomingiz oʻng tomonidagi menyudan (uch nuqta) foydalaning. Bu xatti-harakat taxallus manzillariga qoʻllanmaydi.", "sieve_desc": "Qisqa tavsif", "sieve_type": "Filtr turi", - "skipcrossduplicates": "Papkalararo dublikat xabarlarni oʻtkazib yuborish (birinchi kelgan, birinchi xizmatlanadi)", "sogo_access": "SOGo ga toʻgʻridan-toʻgʻri yoʻnaltirish", "sogo_access_info": "Tizimga kirgandan keyin foydalanuvchi avtomatik ravishda SOGo ga yoʻnaltiriladi.", "sogo_visible": "Taxallus SOGo da koʻrinadi", @@ -777,12 +753,8 @@ "spam_filter": "Spam filtri", "spam_policy": "Elementlarni ruxsat etilgan/qora roʻyxatga qoʻshish yoki olib tashlash", "spam_score": "Maxsus spam bahosini oʻrnatish", - "subfolder2": "Manzilning subpapkasiga sinxronizatsiya qilish
(boʻsh = subpapkadan foydalanmang)", - "syncjob": "Sinxronizatsiya vazifasini tahrirlash", "target_address": "Yoʻnaltirish manzil(lar)i (vergul bilan ajratilgan)", "target_domain": "Maqsad domen", - "timeout1": "Masofaviy xostga ulanish vaqtining tugashi", - "timeout2": "Mahalliy xostga ulanish vaqtining tugashi", "title": "Obyektni tahrirlash", "unchanged_if_empty": "Oʻzgartirilmagan boʻlsa, boʻsh qoldiring", "username": "Foydalanuvchi nomi", @@ -1003,16 +975,6 @@ "spam_aliases": "Vaqt. taxallus", "stats": "Statistika", "status": "Holat", - "sync_jobs": "Sinxronizatsiya vazifalari", - "syncjob_check_log": "Jurnalni tekshirish", - "syncjob_last_run_result": "Oxirgi ishga tushirilgan natija", - "syncjob_EX_OK": "Muvaffaqiyatli", - "syncjob_EXIT_CONNECTION_FAILURE": "Ulanish muammosi", - "syncjob_EXIT_TLS_FAILURE": "Shifrlangan ulanish bilan muammo", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Autentifikatsiya muammosi", - "syncjob_EXIT_OVERQUOTA": "Maqsad pochta qutisi kvotadan oshib ketdi", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Masofaviy serverga ulanib boʻlmadi", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Notoʻgʻri foydalanuvchi nomi yoki parol", "table_size": "Jadval oʻlchami", "table_size_show_n": "%s ta element koʻrsatish", "target_address": "Yoʻnaltirish manzili", @@ -1249,6 +1211,37 @@ "waiting_usb_register": "USB qurilmasi kutilmoqda...

Iltimos, yuqorida parolingizni kiriting va USB qurilmangizdagi tugmani bosib roʻyxatga olinishingizni tasdiqlang.", "yubi_otp": "Yubico OTP autentifikatsiya" }, + "syncjobs": { + "sync_jobs": "Sinxronizatsiya vazifalari", + "syncjob_check_log": "Jurnalni tekshirish", + "syncjob_last_run_result": "Oxirgi ishga tushirilgan natija", + "syncjob_EX_OK": "Muvaffaqiyatli", + "syncjob_EXIT_CONNECTION_FAILURE": "Ulanish muammosi", + "syncjob_EXIT_TLS_FAILURE": "Shifrlangan ulanish bilan muammo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Autentifikatsiya muammosi", + "syncjob_EXIT_OVERQUOTA": "Maqsad pochta qutisi kvotadan oshib ketdi", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Masofaviy serverga ulanib boʻlmadi", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Notoʻgʻri foydalanuvchi nomi yoki parol", + "add": "Sinxronizatsiya vazifasi qoʻshish", + "add_hint": "Parollar oddiy matn sifatida saqlanishini unutmang!", + "edit": "Sinxronizatsiya vazifasini tahrirlash", + "create": "Yangi sinxronizatsiya vazifasi yaratish", + "dry": "Sinxronizatsiyani simulyatsiya qilish", + "subfolder2": "Manzilning subpapkasiga sinxronizatsiya qilish
(boʻsh = subpapkadan foydalanmang)", + "mins_interval": "Soʻrov oraligʻi (daqiqa)", + "maxage": "Masofadan soʻraladigan xabarlarning maksimal yoshi (kunlarda)
(0 = yoshni e'tiborsiz qoldirish)", + "exclude": "Obyektlarni istisno qilish (regex)", + "automap": "Papkalarni avtomatik xaritalashga harakat qilish (\"Sent items\", \"Sent\" => \"Sent\" va h.k.)", + "delete1": "Tugatilgach manbadan oʻchirish", + "delete2": "Manbada boʻlmagan xabarlarni manzildan oʻchirish", + "delete2duplicates": "Manzildagi dublikatlarni oʻchirish", + "skipcrossduplicates": "Papkalararo dublikat xabarlarni oʻtkazib yuborish (birinchi kelgan, birinchi xizmatlanadi)", + "subscribeall": "Barcha papkalarga obuna boʻlish", + "timeout1": "Masofaviy xostga ulanish vaqtining tugashi", + "timeout2": "Mahalliy xostga ulanish vaqtining tugashi", + "maxbytespersecond": "Maks. soniyaga bayt
(0 = cheksiz)", + "custom_params": "Maxsus parametrlar" + }, "user": { "action": "Amal", "active": "Faol", @@ -1279,7 +1272,6 @@ "clear_recent_successful_connections": "Koʻrilgan muvaffaqiyatli ulanishlarni tozalash", "client_configuration": "E-pochta mijozlari va smartfonlar uchun konfiguratsiya qoʻllanmalarini koʻrsatish", "create_app_passwd": "Ilova paroli yaratish", - "create_syncjob": "Yangi sinxronizatsiya vazifasi yaratish", "created_on": "Yaratilgan sana", "daily": "Kunlik", "day": "kun", @@ -1389,16 +1381,6 @@ "spamfilter_wl_desc": "Oq roʻyxatdagi e-pochta manzillari hech qachon spam sifatida tasniflanmaydi. Joker belgilar ishlatilishi mumkin. Filtr faqat toʻgʻridan-toʻgʻri taxalluslarga (bitta maqsad pochta qutisiga ega taxalluslarga) qoʻllaniladi, hammasini-qabul qilish taxalluslari va pochta qutisining oʻzi istisno qilinadi.", "spamfilter_yellow": "Sariq: bu xabar spam boʻlishi mumkin, spam sifatida belgilanadi va Junk papkangizga koʻchiriladi", "status": "Holat", - "sync_jobs": "Sinxronizatsiya vazifalari", - "syncjob_check_log": "Jurnalni tekshirish", - "syncjob_last_run_result": "Oxirgi ishga tushirilgan natija", - "syncjob_EX_OK": "Muvaffaqiyatli", - "syncjob_EXIT_CONNECTION_FAILURE": "Ulanish muammosi", - "syncjob_EXIT_TLS_FAILURE": "Shifrlangan ulanish bilan muammo", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Autentifikatsiya muammosi", - "syncjob_EXIT_OVERQUOTA": "Maqsad pochta qutisi kvotadan oshib ketdi", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Masofaviy serverga ulanib boʻlmadi", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Notoʻgʻri foydalanuvchi nomi yoki parol", "tag_handling": "Teglangan xat uchun ishlovni oʻrnatish", "tag_help_example": "Teglangan e-pochta manzili misoli: me+Facebook@example.org", "tag_help_explain": "Subpapkada: tegga qarab nomlangan yangi subpapka INBOX ostida yaratiladi (\"INBOX/Facebook\").
\r\nMavzuda: teg nomi xatning mavzusiga oldindan qoʻshiladi, misol: \"[Facebook] Mening yangiliklarim\".", diff --git a/data/web/lang/lang.vi-vn.json b/data/web/lang/lang.vi-vn.json index 42813fd25..b750d631a 100644 --- a/data/web/lang/lang.vi-vn.json +++ b/data/web/lang/lang.vi-vn.json @@ -44,24 +44,16 @@ "app_name": "Tên ứng dụng", "app_password": "Thêm mật khẩu ứng dụng", "app_passwd_protocols": "Các giao thức được phép cho mật khẩu ứng dụng", - "automap": "Thử tự động ánh xạ thư mục (\"Mục đã gửi\", \"Đã gửi\" => \"Đã gửi\" v.v.)", "backup_mx_options": "Tùy chọn chuyển tiếp", "bcc_dest_format": "Địa chỉ BCC phải là một địa chỉ email hợp lệ duy nhất.
Nếu bạn cần gửi bản sao đến nhiều địa chỉ, hãy tạo một bí danh và sử dụng nó ở đây.", "comment_info": "Bình luận riêng tư không hiển thị với người dùng, trong khi bình luận công khai được hiển thị dưới dạng chú thích khi di chuột qua trong tổng quan người dùng", - "custom_params": "Tham số tùy chỉnh", - "custom_params_hint": "Đúng: --param=xy, sai: --param xy", - "delete1": "Xóa từ nguồn khi hoàn thành", - "delete2": "Xóa thư ở đích không có ở nguồn", - "delete2duplicates": "Xóa các bản sao ở đích", "description": "Mô tả", "destination": "Đích", "disable_login": "Không cho phép đăng nhập (vẫn nhận được thư đến)", "domain": "Tên miền", "domain_matches_hostname": "Tên miền %s khớp với tên máy chủ", "domain_quota_m": "Tổng hạn ngạch tên miền (MiB)", - "dry": "Mô phỏng đồng bộ hóa", "enc_method": "Phương thức mã hóa", - "exclude": "Loại trừ đối tượng (biểu thức chính quy)", "full_name": "Tên đầy đủ", "gal": "Danh sách địa chỉ toàn cục", "gal_info": "GAL chứa tất cả các đối tượng của một tên miền và không thể được chỉnh sửa bởi bất kỳ người dùng nào. Thông tin rảnh/bận trong SOGo sẽ bị thiếu nếu vô hiệu hóa! Khởi động lại SOGo để áp dụng thay đổi.", @@ -79,7 +71,6 @@ "mailbox_username": "Tên người dùng (phần bên trái của địa chỉ email)", "max_aliases": "Số lượng bí danh tối đa có thể tạo", "max_mailboxes": "Số lượng hộp thư tối đa có thể tạo", - "mins_interval": "Khoảng thời gian kiểm tra (phút)", "multiple_bookings": "Đặt chỗ nhiều lần", "nexthop": "Bước nhảy tiếp theo", "password": "Mật khẩu", @@ -99,16 +90,10 @@ "select_domain": "Vui lòng chọn tên miền trước", "sieve_desc": "Mô tả ngắn", "sieve_type": "Loại bộ lọc", - "skipcrossduplicates": "Bỏ qua tin nhắn trùng lặp giữa các thư mục (ai đến trước được phục vụ trước)", - "subscribeall": "Đăng ký tất cả thư mục", - "syncjob": "Thêm công việc đồng bộ", - "syncjob_hint": "Lưu ý rằng mật khẩu cần được lưu dưới dạng văn bản thuần!", "tags": "Thẻ", "target_address": "Địa chỉ chuyển tiếp", "target_address_info": "Địa chỉ email đầy đủ (phân cách bằng dấu phẩy).", "target_domain": "Tên miền đích", - "timeout1": "Thời gian chờ kết nối đến máy chủ từ xa", - "timeout2": "Thời gian chờ kết nối đến máy chủ cục bộ", "username": "Tên người dùng", "validate": "Xác thực", "validation_success": "Xác thực thành công", @@ -646,7 +631,6 @@ "app_name": "Tên ứng dụng", "app_passwd": "Mật khẩu ứng dụng", "app_passwd_protocols": "Các giao thức được phép cho mật khẩu ứng dụng", - "automap": "Thử tự động ánh xạ thư mục (\"Mục đã gửi\", \"Đã gửi\" => \"Đã gửi\" v.v.)", "backup_mx_options": "Tùy chọn chuyển tiếp", "bcc_dest_format": "Đích BCC phải là một địa chỉ email hợp lệ duy nhất.
Nếu bạn cần gửi bản sao đến nhiều địa chỉ, hãy tạo một bí danh và sử dụng nó ở đây.", "client_id": "ID khách hàng", @@ -654,9 +638,6 @@ "comment_info": "Bình luận riêng tư không hiển thị với người dùng, trong khi bình luận công khai được hiển thị dưới dạng chú thích khi di chuột qua trong tổng quan người dùng", "created_on": "Được tạo vào", "custom_attributes": "Thuộc tính tùy chỉnh", - "delete1": "Xóa từ nguồn khi hoàn thành", - "delete2": "Xóa thư ở đích không có ở nguồn", - "delete2duplicates": "Xóa các bản sao ở đích", "delete_ays": "Vui lòng xác nhận quá trình xóa.", "description": "Mô tả", "disable_login": "Không cho phép đăng nhập (vẫn nhận được thư đến)", @@ -680,7 +661,6 @@ "dont_check_sender_acl": "Tắt kiểm tra người gửi cho tên miền %s (+ tên miền bí danh)", "edit_alias_domain": "Chỉnh sửa tên miền bí danh", "encryption": "Mã hóa", - "exclude": "Loại trừ đối tượng (biểu thức chính quy)", "extended_sender_acl": "Địa chỉ người gửi bên ngoài", "extended_sender_acl_info": "Khóa tên miền DKIM nên được nhập vào, nếu có sẵn.
\n Nhớ thêm máy chủ này vào bản ghi SPF TXT tương ứng.
\n Khi một tên miền hoặc tên miền bí danh được thêm vào máy chủ này, mà trùng với một địa chỉ bên ngoài, địa chỉ bên ngoài sẽ bị xóa.
\n Sử dụng @domain.tld để cho phép gửi dưới dạng *@domain.tld.", "force_pw_update": "Bắt buộc cập nhật mật khẩu trong lần đăng nhập tiếp theo", @@ -711,5 +691,21 @@ "max_aliases": "Số lượng Bí danh tối đa", "max_mailboxes": "Số lượng hộp thư tối đa có thể có", "max_quota": "Dung lượng tối đa cho mỗi hộp thư (MiB)" + }, + "syncjobs": { + "add": "Thêm công việc đồng bộ", + "add_hint": "Lưu ý rằng mật khẩu cần được lưu dưới dạng văn bản thuần!", + "dry": "Mô phỏng đồng bộ hóa", + "mins_interval": "Khoảng thời gian kiểm tra (phút)", + "exclude": "Loại trừ đối tượng (biểu thức chính quy)", + "automap": "Thử tự động ánh xạ thư mục (\"Mục đã gửi\", \"Đã gửi\" => \"Đã gửi\" v.v.)", + "delete1": "Xóa từ nguồn khi hoàn thành", + "delete2": "Xóa thư ở đích không có ở nguồn", + "delete2duplicates": "Xóa các bản sao ở đích", + "skipcrossduplicates": "Bỏ qua tin nhắn trùng lặp giữa các thư mục (ai đến trước được phục vụ trước)", + "subscribeall": "Đăng ký tất cả thư mục", + "timeout1": "Thời gian chờ kết nối đến máy chủ từ xa", + "timeout2": "Thời gian chờ kết nối đến máy chủ cục bộ", + "custom_params": "Tham số tùy chỉnh" } } diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index 2a1a080b9..6b333e57d 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -45,15 +45,9 @@ "app_name": "应用名称", "app_password": "添加应用密码", "app_passwd_protocols": "应用密码允许的协议", - "automap": "将尝试自动映射文件夹 (例如将 \"已发送消息\" 和 \"已发送\" 均映射到 \"已发送\" 文件夹)", "backup_mx_options": "中继选项", "bcc_dest_format": "BCC 的目标地址必须是一个有效的电子邮箱地址。
如果你需要向多个地址发送副本,请创建一个别名并在此使用。", "comment_info": "私密评论对用户不可见,公开评论将会在鼠标悬停时作为用户提示显示", - "custom_params": "自定义参数", - "custom_params_hint": "正确的格式: --param=xy ,错误的格式: --param xy", - "delete1": "在完成后删除源邮件", - "delete2": "删除在目标邮箱中存在但在源邮箱中不存在的邮件", - "delete2duplicates": "删除目标邮箱中的重复邮件", "description": "描述", "destination": "目标邮箱", "disable_login": "不允许登录 (但仍然会接收邮件)", @@ -61,7 +55,6 @@ "domain_matches_hostname": "域名 %s 与主机名称匹配", "domain_quota_m": "域名总配额 (MiB)", "enc_method": "加密方法", - "exclude": "拒绝对象 (Regex)", "full_name": "全称", "gal": "全球地址簿", "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 \"空闲/繁忙\" 的状态将无法在 SOGo 中显示。 重启 SOGo 服务以应用更改。", @@ -77,7 +70,6 @@ "mailbox_username": "用户名 (邮箱地址左侧的部分)", "max_aliases": "最大允许的地址别名数", "max_mailboxes": "最大允许的邮箱数", - "mins_interval": "轮询间隔 (分钟)", "multiple_bookings": "登记限制", "nexthop": "下一跳", "password": "密码", @@ -97,20 +89,13 @@ "select_domain": "请先选择一个域名", "sieve_desc": "简短描述", "sieve_type": "过滤器类型", - "skipcrossduplicates": "跳过其他文件夹中已存在的邮件 (保留已经存在的邮件)", - "subscribeall": "订阅所有文件夹", - "syncjob": "添加同步任务", - "syncjob_hint": "注意密码将以明文存储!", "tags": "标签", "target_address": "目标地址", "target_address_info": "完整的邮箱地址 (存在多个地址时使用英文逗号分隔)", "target_domain": "目标域名", - "timeout1": "远程主机连接超时时间", - "timeout2": "本地主机连接超时时间", "username": "用户名", "validate": "验证", "validation_success": "验证成功", - "dry": "模拟同步(Dry run)", "internal_info": "内部的别名只能在域内部或者别名域内部访问。", "internal": "内部的", "sender_allowed": "允许以该别名发送" @@ -623,15 +608,11 @@ "app_name": "应用名称", "app_passwd": "应用密码", "app_passwd_protocols": "应用密码允许的协议", - "automap": "将尝试自动映射文件夹 (例如将 \"已发送消息\" 和 \"已发送\" 均映射到 \"已发送\" 文件夹)", "backup_mx_options": "中继选项", "bcc_dest_format": "BCC 的目标必须是一个有效的电子邮箱地址。
如果你需要向多个地址发送副本,请创建一个别名并在此使用。", "client_id": "客户端 ID", "client_secret": "客户端 secret", "comment_info": "私密评论对用户不可见,公开评论将会在鼠标悬停时作为用户提示显示", - "delete1": "在完成后删除源邮件", - "delete2": "删除在目标邮箱中存在但在源邮箱中不存在的邮件", - "delete2duplicates": "删除目标邮箱中的重复邮件", "delete_ays": "请确认删除。", "description": "描述", "disable_login": "不允许登录 (但仍然会接收邮件)", @@ -642,7 +623,6 @@ "dont_check_sender_acl": "为域名 %s (+ 域名别名) 关闭发件人检查", "edit_alias_domain": "编辑域名别名", "encryption": "加密", - "exclude": "排除对象 (Regex)", "extended_sender_acl": "外部发件人地址", "extended_sender_acl_info": "当可用时请导入域名的 DKIM 密钥。
\r\n 请注意将此服务器添加到相应的 SPF TXT 记录中。
\r\n 当域名或域名别名被添加时,若其与此外部发件人地址交叠,则外部发件人地址将会被移除。
\r\n 填入 @domain.tld 以允许作为 *@domain.tld 发送邮件。", "force_pw_update": "在下一次登录时强制要求更新密码", @@ -662,10 +642,7 @@ "max_aliases": "最大允许的地址别名数", "max_mailboxes": "最大允许的邮箱数", "max_quota": "每个邮箱的最大配额 (MiB)", - "maxage": "从远程拉取消息的最大时间间隔限制
(0表示忽略)", - "maxbytespersecond": "最大速率 (Bytes/s)
(0表示无限制)", "mbox_rl_info": "此频率限制应用于 SASL 登录名,它会匹配邮件的 \"from\" 地址。邮箱的频率限制设置会覆盖域名的频率限制值设置。", - "mins_interval": "轮询间隔 (分钟)", "multiple_bookings": "登记限制", "none_inherit": "无/继承", "nexthop": "下一跳", @@ -704,7 +681,6 @@ "sender_acl_info": "如果允许邮箱用户 A 作为邮箱用户 B 发送邮件,发件人的地址不会在 SOGo 中的 \"from\" 区域中作为下拉项显示。
\r\n 邮箱用户 B 需要添加授权以允许邮箱用户 A 选择 B 的地址作为发件人;授权方法为,在 SOGo 中点击左上方邮箱地址右边的菜单按钮 (三个点) 并授权。", "sieve_desc": "简短描述", "sieve_type": "过滤器类型", - "skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留已经存在的邮件)", "sogo_access": "直接转到 SOGo", "sogo_access_info": "登录后,用户会自动跳转到 SOGo。", "sogo_visible": "SOGo 显示的别名", @@ -713,12 +689,8 @@ "spam_filter": "垃圾邮件过滤器", "spam_policy": "将项目添加到白/黑名单或从其中移除", "spam_score": "自定义垃圾邮件分数", - "subfolder2": "同步到目标邮箱的子文件夹
(留空表示不使用子文件夹)", - "syncjob": "编辑同步任务", "target_address": "目标地址 (存在多个地址时使用英文逗号分隔)", "target_domain": "目标域名", - "timeout1": "远程主机连接超时时间", - "timeout2": "本地主机连接超时时间", "title": "编辑对象", "unchanged_if_empty": "如果不更改则留空", "username": "用户名", @@ -964,16 +936,6 @@ "spam_aliases": "临时别名", "stats": "统计", "status": "状态", - "sync_jobs": "同步任务", - "syncjob_check_log": "检查日志", - "syncjob_last_run_result": "最后一次运行结果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "连接问题", - "syncjob_EXIT_TLS_FAILURE": "加密连接问题", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "身份认证问题", - "syncjob_EXIT_OVERQUOTA": "目标邮箱配额已满", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "无法连接到远程服务器", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "用户名或密码错误", "table_size": "表格尺寸", "table_size_show_n": "显示 %s 个项目", "target_address": "目标地址", @@ -1226,6 +1188,37 @@ "setup_required": "你的账户需要设置两步验证。请添加一个两步验证方法以继续。", "cancel_setup": "取消并注销" }, + "syncjobs": { + "sync_jobs": "同步任务", + "syncjob_check_log": "检查日志", + "syncjob_last_run_result": "最后一次运行结果", + "syncjob_EX_OK": "成功", + "syncjob_EXIT_CONNECTION_FAILURE": "连接问题", + "syncjob_EXIT_TLS_FAILURE": "加密连接问题", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "身份认证问题", + "syncjob_EXIT_OVERQUOTA": "目标邮箱配额已满", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "无法连接到远程服务器", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "用户名或密码错误", + "add": "添加同步任务", + "add_hint": "注意密码将以明文存储!", + "edit": "编辑同步任务", + "create": "添加同步任务", + "dry": "模拟同步(Dry run)", + "subfolder2": "同步到目标邮箱的子文件夹
(留空表示不使用子文件夹)", + "mins_interval": "轮询间隔 (分钟)", + "maxage": "从远程拉取消息的最大时间间隔限制
(0表示忽略)", + "exclude": "拒绝对象 (Regex)", + "automap": "将尝试自动映射文件夹 (例如将 \"已发送消息\" 和 \"已发送\" 均映射到 \"已发送\" 文件夹)", + "delete1": "在完成后删除源邮件", + "delete2": "删除在目标邮箱中存在但在源邮箱中不存在的邮件", + "delete2duplicates": "删除目标邮箱中的重复邮件", + "skipcrossduplicates": "跳过其他文件夹中已存在的邮件 (保留已经存在的邮件)", + "subscribeall": "订阅所有文件夹", + "timeout1": "远程主机连接超时时间", + "timeout2": "本地主机连接超时时间", + "maxbytespersecond": "最大速率 (Bytes/s)
(0表示无限制)", + "custom_params": "自定义参数" + }, "user": { "action": "操作", "active": "启用", @@ -1254,7 +1247,6 @@ "clear_recent_successful_connections": "清除成功匹配的连接", "client_configuration": "显示邮箱客户端和智能手机的配置指南", "create_app_passwd": "添加应用密码", - "create_syncjob": "添加同步任务", "created_on": "添加于", "daily": "每日", "day": "日", @@ -1355,16 +1347,6 @@ "spamfilter_wl_desc": "允许名单中地址永远不会被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", "spamfilter_yellow": "黄色: 此为垃圾邮件,会被标记为垃圾邮件并且移入垃圾邮件文件夹", "status": "状态", - "sync_jobs": "同步任务", - "syncjob_check_log": "检查日志", - "syncjob_last_run_result": "最后一次运行结果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "连接问题", - "syncjob_EXIT_TLS_FAILURE": "加密连接问题", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "身份认证问题", - "syncjob_EXIT_OVERQUOTA": "目标邮箱配额已满", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "无法连接到远程服务器", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "用户名或密码错误", "tag_handling": "处理带有标签的邮件", "tag_help_example": "带有标签的邮箱地址示例: me+Facebook@example.org", "tag_help_explain": "置于子文件夹: 在 INBOX (收件箱) 下创建一个以标签名命名的子文件夹 (\"INBOX/Facebook\")。
\r\n置于主题: 标签名会被前置到邮件主题, 例如: \"[Facebook] My News\"。", diff --git a/data/web/lang/lang.zh-tw.json b/data/web/lang/lang.zh-tw.json index e7310be7a..3cb2d8170 100644 --- a/data/web/lang/lang.zh-tw.json +++ b/data/web/lang/lang.zh-tw.json @@ -44,15 +44,9 @@ "app_name": "應用程式名稱", "app_password": "新增應用程式密碼", "app_passwd_protocols": "此應用程式密碼所允許的協定", - "automap": "自動歸類資料夾規則表 (如:\"已發送\", \"Sent\" => \"Sent\" 等...)", "backup_mx_options": "中繼選項", "bcc_dest_format": "密件副本地址必須是單一的有效地址。
如果你需要傳送到多個地址,請幫它們建立一個別名然後設定於此欄。", "comment_info": "隱密備註不會被使用者看到,公開備註則會在使用者游標懸停於概述頁時顯示於提示框", - "custom_params": "訂製參數", - "custom_params_hint": "正確的用法:--param=xy,錯誤的用法:--param xy", - "delete1": "完成後將來源郵件刪除", - "delete2": "刪除目的地信箱中存在但來源信箱中不存在的郵件", - "delete2duplicates": "刪除目的地信箱中的重複郵件", "description": "描述", "destination": "目的地信箱", "disable_login": "不允許登入 (但郵件仍然會正常接收)", @@ -60,7 +54,6 @@ "domain_matches_hostname": "域名 %s 與主機名稱匹配", "domain_quota_m": "域名總容量配額 (MiB)", "enc_method": "加密方式", - "exclude": "拒絕物件 (regex)", "full_name": "全名", "gal": "全域地址清單", "gal_info": "全域地址清單包含了域名下所有的物件,且它們不能被使用者更改。如果關閉,使用者的 閒置/忙碌中 狀態將無法在 SOGo 中顯示。 需重新啟動 SOGo 來更改。", @@ -76,7 +69,6 @@ "mailbox_username": "使用者名稱 (郵件地址的左側部分)", "max_aliases": "地址別名上限", "max_mailboxes": "信箱數量上限", - "mins_interval": "輪詢間隔 (分鐘)", "multiple_bookings": "允許重複預約", "nexthop": "下一個跳躍點", "password": "密碼", @@ -96,20 +88,13 @@ "select_domain": "請先選擇一個域名", "sieve_desc": "簡短描述", "sieve_type": "過濾器類型", - "skipcrossduplicates": "跳過在其他資料夾中重複的郵件 (優先使用先找到的郵件)", - "subscribeall": "訂閱所有資料夾", - "syncjob": "新增同步任務", - "syncjob_hint": "請注意此密碼會以明文儲存!", "tags": "標籤", "target_address": "目標地址", "target_address_info": "完整的信箱地址 (以英文逗號分隔多個地址)。", "target_domain": "目標域名", - "timeout1": "遠端主機連線逾時時間", - "timeout2": "本地主機連線逾時時間", "username": "使用者名稱", "validate": "驗證", - "validation_success": "驗證成功", - "dry": "模擬同步" + "validation_success": "驗證成功" }, "admin": { "access": "存取", @@ -557,16 +542,12 @@ "app_name": "應用程式名稱", "app_passwd": "應用程式密碼", "app_passwd_protocols": "應用程式密碼允許的協定", - "automap": "自動分類資料夾規則 (如:\"已發送\", \"寄件備份\" => \"Sent\")", "backup_mx_options": "中繼選項", "bcc_dest_format": "密件副本地址必須是單一的有效地址。
如果你需要傳送到多個地址,請幫它們建立一個別名然後設定於此欄。", "client_id": "用戶端 ID", "client_secret": "用戶端金鑰", "comment_info": "隱密備註不會被使用者看到,公開備註則會在使用者游標懸停於概述頁時顯示於提示框", "created_on": "建立於", - "delete1": "完成後將來源郵件刪除", - "delete2": "刪除目的地信箱中存在但來源信箱中不存在的郵件", - "delete2duplicates": "刪除目的地信箱中的重複郵件", "delete_ays": "確定要刪除?", "description": "描述", "disable_login": "不允許登入 (但郵件仍然會正常接收)", @@ -577,7 +558,6 @@ "dont_check_sender_acl": "關閉域名 %s (及其域名別名) 的寄件人檢查", "edit_alias_domain": "編輯域名別名", "encryption": "加密", - "exclude": "拒絕物件 (正規表示式)", "extended_sender_acl": "外部寄件人地址", "extended_sender_acl_info": "如果可以的話,請匯入 DKIM 域名金鑰。
\r\n別忘記將此伺服器新增到相應的 SPF TXT 中。
\r\n當域名或域名別名被新增時,若其與此外部寄件人地址交疊,則外部寄件人地址會被移除。
\r\n填入 @domain.tld 以允許作為 *@domain.tld 發送郵件。", "force_pw_update": "在下一次登入時強制要求更新密碼", @@ -599,10 +579,7 @@ "max_aliases": "最大允許地址別名數", "max_mailboxes": "最大允許信箱數", "max_quota": "每個信箱的最大容量配額 (MiB)", - "maxage": "從遠端拉取消息的最大消息年齡限制
(0 表示忽略)", - "maxbytespersecond": "最大速率 (Bytes/s)
(0 表示不限)", "mbox_rl_info": "此頻率限制應用於 SASL 登入名,它會匹配任何郵件的\"寄件人\"地址。信箱的頻率限制設定會覆蓋域名的速率限制值設定。", - "mins_interval": "輪詢間隔 (分鐘)", "multiple_bookings": "多重登記限制", "none_inherit": "無 / 繼承", "nexthop": "下一跳", @@ -641,7 +618,6 @@ "sender_acl_info": "如果信箱使用者 A 被允許以信箱使用者 B 發送郵件,該寄件人地址不會出現在 SOGo 中\"發送自\"的下拉選項中。
\r\n信箱使用者 B 需要新增授權以允許信箱使用者 A 選擇 B 的地址作為寄件人;授權方法為,在 SOGo 中點擊右上方信箱名稱左邊的選項按鈕(三個點)並授權。此行為不會套用於信箱別名。", "sieve_desc": "簡短描述", "sieve_type": "過濾器類型", - "skipcrossduplicates": "跳過在其他資料夾中重複的郵件 (優先使用先找到的郵件)", "sogo_access": "直接轉寄至SOGo", "sogo_access_info": "登入後,使用者會自動重新導向至SOGo。", "sogo_visible": "別名會在 SOGo 中顯示", @@ -650,12 +626,8 @@ "spam_filter": "垃圾郵件過濾器", "spam_policy": "將項目新增到白/黑名單或從其中移除", "spam_score": "自訂垃圾郵件分數", - "subfolder2": "同步到目標信箱子資料夾
(留空表示不使用子資料夾)", - "syncjob": "編輯同步任務", "target_address": "目標地址 (以英文逗號分隔多個地址)", "target_domain": "目標域名", - "timeout1": "遠端主機連接超時時間", - "timeout2": "本地主機連接超時時間", "title": "編輯物件", "unchanged_if_empty": "如果不更改則留空", "username": "使用者名稱", @@ -877,16 +849,6 @@ "spam_aliases": "臨時別名", "stats": "統計", "status": "狀態", - "sync_jobs": "同步任務", - "syncjob_check_log": "檢查日誌", - "syncjob_last_run_result": "上次執行結果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "連線問題", - "syncjob_EXIT_TLS_FAILURE": "加密連線問題", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "身分驗證問題", - "syncjob_EXIT_OVERQUOTA": "目標信箱容量配額已滿", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "無法連接至遠端伺服器", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "錯誤的使用者名稱或密碼", "table_size": "表格尺寸", "table_size_show_n": "顯示 %s 個項目", "target_address": "目標地址", @@ -1103,6 +1065,37 @@ "yubi_otp": "Yubico OTP 認證", "authenticators": "驗證器(Authenticators)" }, + "syncjobs": { + "sync_jobs": "同步任務", + "syncjob_check_log": "檢查日誌", + "syncjob_last_run_result": "上一次執行結果", + "syncjob_EX_OK": "成功", + "syncjob_EXIT_CONNECTION_FAILURE": "連線問題", + "syncjob_EXIT_TLS_FAILURE": "加密連線問題", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "身分認證問題", + "syncjob_EXIT_OVERQUOTA": "目標信箱容量配額已滿", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "無法連接至遠端伺服器", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "錯誤的使用者名稱或密碼", + "add": "新增同步任務", + "add_hint": "請注意此密碼會以明文儲存!", + "edit": "編輯同步任務", + "create": "新增同步任務", + "dry": "模擬同步", + "subfolder2": "同步到目標信箱子資料夾
(留空表示不使用子資料夾)", + "mins_interval": "輪詢間隔 (分鐘)", + "maxage": "從遠端拉取消息的最大消息年齡限制
(0 表示忽略)", + "exclude": "拒絕物件 (regex)", + "automap": "自動歸類資料夾規則表 (如:\"已發送\", \"Sent\" => \"Sent\" 等...)", + "delete1": "完成後將來源郵件刪除", + "delete2": "刪除目的地信箱中存在但來源信箱中不存在的郵件", + "delete2duplicates": "刪除目的地信箱中的重複郵件", + "skipcrossduplicates": "跳過在其他資料夾中重複的郵件 (優先使用先找到的郵件)", + "subscribeall": "訂閱所有資料夾", + "timeout1": "遠端主機連線逾時時間", + "timeout2": "本地主機連線逾時時間", + "maxbytespersecond": "最大速率 (Bytes/s)
(0 表示不限)", + "custom_params": "訂製參數" + }, "user": { "action": "操作", "active": "啟用", @@ -1131,7 +1124,6 @@ "clear_recent_successful_connections": "中斷成功的連線", "client_configuration": "顯示電子信箱程式和智慧型手機的設定指南", "create_app_passwd": "新增應用程式密碼", - "create_syncjob": "新增同步任務", "created_on": "建立於", "daily": "每日", "day": "日", @@ -1232,16 +1224,6 @@ "spamfilter_wl_desc": "白名單中地址永遠不會被標記為垃圾郵件。可以使用萬用匹配字元\"*\"。此過濾器只會套用於直接別名 (只有一個目標信箱),而不會應用到\"接收所有信件\"別名和信箱地址本身。", "spamfilter_yellow": "黃色: 此信件可能是垃圾消息,會被標記為垃圾信件並且移入垃圾信件資料夾", "status": "狀態", - "sync_jobs": "同步任務", - "syncjob_check_log": "檢查日誌", - "syncjob_last_run_result": "上一次執行結果", - "syncjob_EX_OK": "成功", - "syncjob_EXIT_CONNECTION_FAILURE": "連線問題", - "syncjob_EXIT_TLS_FAILURE": "加密連線問題", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "身分認證問題", - "syncjob_EXIT_OVERQUOTA": "目標信箱容量配額已滿", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "無法連接至遠端伺服器", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "錯誤的使用者名稱或密碼", "tag_handling": "設定被標籤的郵件處理方式", "tag_help_example": "標籤的信箱地址範例: me+Facebook@example.org", "tag_help_explain": "置於子資料夾: 在收件夾下創建一個以標籤名命名的子資料夾 (\"INBOX/Facebook\")。
\r\n主旨: 標籤名會被前綴於郵件主旨, 如: \"[Facebook] 我的新聞\"。", diff --git a/data/web/syncjob-oauth.php b/data/web/syncjob-oauth.php new file mode 100644 index 000000000..1046dc97a --- /dev/null +++ b/data/web/syncjob-oauth.php @@ -0,0 +1,103 @@ + redirect the logged-in user to the provider consent screen +// (callback, ?code&state) -> exchange code, store the token for mailbox M, message the opener +// The token is bound to the local mailbox M (owner) and the authenticated remote identity (syncjob user1). +// The redirect URI is always https://MAILCOW_HOSTNAME/syncjob-oauth, so the UI must be used on that host. +// postMessage only carries short error codes, the UI translates them: +// access_denied, mailbox_denied, source_unavailable, provider_denied, invalid_state, token_failed, no_identity +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; + +function syncjob_oauth_finish($ok, $payload) { + // Renders a tiny page that hands the result back to the opener modal and closes the popup. + global $lang; + header('Content-Type: text/html; charset=utf-8'); + header('Cache-Control: no-store'); + $json = json_encode(array_merge(array('type' => 'syncjob_oauth', 'ok' => $ok), $payload), + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); + $title = $lang['syncjobs']['oauth_popup_title'] ?? 'Sign in with the provider'; + $text = $ok + ? ($lang['syncjobs']['oauth_popup_success'] ?? 'Connected. You can close this window.') + : ($lang['syncjobs']['oauth_popup_failed'] ?? 'Authentication failed. You can close this window.'); + echo '' . htmlspecialchars($title) . '' + . '

' . htmlspecialchars($text) . '

'; + exit; +} + +// Must be an authenticated session with the syncjobs ACL (user_acl / da_acl; always set for admins). +if (empty($_SESSION['mailcow_cc_username']) || empty($_SESSION['mailcow_cc_role'])) { + header('Location: /'); + exit; +} +$context = \Mailcow\Core\Context::fromSession($_SESSION); +$imapsync = \Mailcow\Imapsync\ImapsyncModule::fromGlobals($context); +if (!$context->hasAcl('syncjobs')) { + syncjob_oauth_finish(false, array('error' => 'access_denied')); +} + +$action = isset($_GET['action']) ? $_GET['action'] : ((isset($_GET['code']) || isset($_GET['error'])) ? 'callback' : ''); + +if ($action == 'start') { + $source_id = intval($_GET['source_id'] ?? 0); + // C1: the token is stored for this local mailbox; default is the own mailbox (role user) + $mailbox = (isset($_GET['mailbox']) && is_string($_GET['mailbox']) && $_GET['mailbox'] !== '') + ? strtolower(trim($_GET['mailbox'])) : $_SESSION['mailcow_cc_username']; + if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL) || !$context->canAccessMailbox($mailbox)) { + syncjob_oauth_finish(false, array('error' => 'mailbox_denied')); + } + // findConnectable enforces visibility for the current session + $state = bin2hex(random_bytes(16)); + $source = $imapsync->sources->findConnectable($source_id); + $url = $source ? $imapsync->oauth->authorizeUrl($source, $state) : null; + if ($url === null) { + syncjob_oauth_finish(false, array('error' => 'source_unavailable')); + } + $_SESSION['syncjob_oauth2state'] = $state; + $_SESSION['syncjob_oauth_source_id'] = $source_id; + $_SESSION['syncjob_oauth_mailbox'] = $mailbox; + header('Location: ' . $url); + exit; +} + +// --- callback --- +// CSRF: state must match the one stored at 'start' +$state_ok = !empty($_GET['state']) && is_string($_GET['state']) + && hash_equals((string)($_SESSION['syncjob_oauth2state'] ?? ''), $_GET['state']); +$source_id = intval($_SESSION['syncjob_oauth_source_id'] ?? 0); +$mailbox = (string)($_SESSION['syncjob_oauth_mailbox'] ?? ''); +if ($state_ok) { + unset($_SESSION['syncjob_oauth2state'], $_SESSION['syncjob_oauth_source_id'], $_SESSION['syncjob_oauth_mailbox']); +} +if (isset($_GET['error'])) { + // Provider error codes are not passed through (only a fixed code) + syncjob_oauth_finish(false, array('error' => 'provider_denied')); +} +if (!$state_ok || empty($_GET['code']) || !is_string($_GET['code']) || $source_id <= 0 || $mailbox === '') { + syncjob_oauth_finish(false, array('error' => 'invalid_state')); +} +// Re-check access to the owner mailbox (session could have changed in between) +if (!$context->canAccessMailbox($mailbox)) { + syncjob_oauth_finish(false, array('error' => 'mailbox_denied')); +} + +$source = $imapsync->sources->findConnectable($source_id); +if (!$source) { + syncjob_oauth_finish(false, array('error' => 'source_unavailable')); +} + +// C1: the token is bound to the local owner mailbox and the remote identity (id_token claims, else userinfo). +// Sanitized details go to the php-fpm log only, the opener gets a fixed code. +try { + $user1 = $imapsync->oauth->connectUser($source, $mailbox, $_GET['code']); +} +catch (\Mailcow\Core\UserError $e) { + if ($e->args) { + error_log('syncjob-oauth: ' . $e->args[0]); + } + syncjob_oauth_finish(false, array('error' => $e->key)); +} +syncjob_oauth_finish(true, array('source_id' => $source_id, 'mailbox' => $mailbox, 'user1' => $user1)); diff --git a/data/web/templates/admin.twig b/data/web/templates/admin.twig index 651107ccf..0c487c4ea 100644 --- a/data/web/templates/admin.twig +++ b/data/web/templates/admin.twig @@ -22,6 +22,7 @@
  • +
  • @@ -51,6 +52,7 @@ {% include 'admin/tab-config-f2b.twig' %} {% include 'admin/tab-config-quarantine.twig' %} {% include 'admin/tab-config-quota.twig' %} + {% include 'admin/tab-config-syncjob.twig' %} {% include 'admin/tab-config-rsettings.twig' %} {% include 'admin/tab-config-customize.twig' %} {% include 'admin/tab-config-password-settings.twig' %} diff --git a/data/web/templates/admin/tab-config-syncjob.twig b/data/web/templates/admin/tab-config-syncjob.twig new file mode 100644 index 000000000..21479d48e --- /dev/null +++ b/data/web/templates/admin/tab-config-syncjob.twig @@ -0,0 +1,35 @@ +
    +
    +
    + + {{ lang.admin.syncjobs }} +
    +
    +
    +
    +
    + + + {{ lang.admin.syncjob_max_parallel_info }} +
    +
    + + + {{ lang.admin.syncjob_max_kb_per_second_info }} +
    +
    + +
    +
    +
    +
    diff --git a/data/web/templates/edit.twig b/data/web/templates/edit.twig index 375018c66..a5c2cc154 100644 --- a/data/web/templates/edit.twig +++ b/data/web/templates/edit.twig @@ -26,6 +26,7 @@ var lang_user = {{ lang_user|raw }}; var lang_admin = {{ lang_admin|raw }}; var lang_datatables = {{ lang_datatables|raw }}; + var lang_syncjobs = {{ lang_syncjobs|raw }}; var csrf_token = '{{ csrf_token }}'; var pagination_size = Math.trunc('{{ pagination_size }}'); var table_for_domain = '{{ domain }}'; diff --git a/data/web/templates/edit/imapsync_source.twig b/data/web/templates/edit/imapsync_source.twig new file mode 100644 index 000000000..d6ad9d601 --- /dev/null +++ b/data/web/templates/edit/imapsync_source.twig @@ -0,0 +1,233 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} + {# XOAUTH2 sources are managed by admins and domain admins only #} + {% set ro = not result.can_edit or (mailcow_cc_role == 'user' and result.auth_type == 'XOAUTH2') %} + {% set show_oauth = mailcow_cc_role != 'user' or result.auth_type == 'XOAUTH2' %} + {% set is_cc = result.auth_type == 'XOAUTH2' and result.oauth_flow != 'authorization_code' %} +

    {{ lang.syncjobs.source }}: {{ result.name }}

    + {% if ro %} + + {% endif %} +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + {% if result.created_by %} +
    + +
    + +
    +
    + {% endif %} + {% if (mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin') and not ro %} + + + {% else %} +
    + +
    + {% set scope_targets = result.scope == 'domain' ? result.domains : (result.scope == 'user' ? result.users : []) %} + +
    +
    + {% endif %} +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + {% if show_oauth %} +
    + {{ lang.syncjobs.source_oauth_block }} +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    + {% if not ro %} + {{ lang.syncjobs.source_redirect_uri_hint }} + {% endif %} +
    +
    +
    +
    + +
    + +
    +
    + {% if not ro %} +
    + +
    + + {{ lang.syncjobs.source_secret_unchanged_hint }} +
    +
    + {% endif %} +
    + +
    + +
    +
    + {% if not ro %} +
    + +
    + + {{ lang.syncjobs.source_oauth_extra_params_hint }} +
    +
    + {% endif %} + {% if is_cc %} + {# token state of the stored client credentials token (authorization_code tokens live per sync job) #} +
    +
    +

    + {% if result.oauth_last_refresh_error %} + {{ lang.syncjobs.token_state_failed }} + {% elseif result.has_access_token %} + {{ lang.syncjobs.token_state_issued }} + {% if result.oauth_token_expires %}{{ lang.syncjobs.source_token_status }}: {{ result.oauth_token_expires|date('Y-m-d H:i:s') }}{% endif %} + {% else %} + {{ lang.syncjobs.token_state_none }} + {% endif %} +

    + {% if result.oauth_last_refresh_error %} +

    {{ lang.syncjobs.source_token_error }}: {{ result.oauth_last_refresh_error }}

    + {% endif %} + {% if not ro %} + {# own data-id: must not harvest/post the form fields #} + + {% endif %} +
    +
    + {% endif %} +
    + {% endif %} +
    +
    +
    + +
    +
    +
    + {% if not ro %} +
    +
    + +
    +
    + {% endif %} +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/data/web/templates/edit/syncjob.twig b/data/web/templates/edit/syncjob.twig index 801009849..8b958079b 100644 --- a/data/web/templates/edit/syncjob.twig +++ b/data/web/templates/edit/syncjob.twig @@ -2,8 +2,8 @@ {% block inner_content %} {% if result %} -

    {{ lang.edit.syncjob }}

    -
    +

    {{ lang.syncjobs.edit }}

    + @@ -13,139 +13,180 @@
    - +
    - +
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - + {% if result.source_id %} + + {% endif %} + {% if not result.source_id %} + + {% endif %} +
    {{ lang.syncjobs.no_sources }}
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + {% if result.user_token_exists == 1 and not result.user_token_error %} + {{ lang.syncjobs.syncjob_oauth_connected }}: {{ result.user1 }} + {% elseif result.user_token_error %} + {{ lang.syncjobs.token_state_failed }}: {{ result.user1 }} + {% else %} + {{ lang.syncjobs.token_state_pending }} + {% endif %} +
    {{ lang.syncjobs.oauth_hint }}
    +
    +
    +
    + +
    + + {{ lang.edit.unchanged_if_empty }}
    - +
    - + 1-43800
    + {% if mailcow_cc_role == 'admin' %}
    - + +
    + + {{ lang.syncjobs.prio_hint }} +
    +
    + {% endif %} +
    +
    - +
    0-32000
    - +
    0-125000000
    - +
    1-32000
    - +
    1-32000
    - +
    - +
    - - {{ lang.add.custom_params_hint }} +
    +
    + +
    + + {{ lang.syncjobs.custom_params_hint }}
    + {% if result.custom_params_legacy %} +
    + +
    +
    {{ lang.syncjobs.custom_params_legacy_hint }}
    + +
    +
    + {% endif %}
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    diff --git a/data/web/templates/mailbox.twig b/data/web/templates/mailbox.twig index 1265d00cc..f330a76b9 100644 --- a/data/web/templates/mailbox.twig +++ b/data/web/templates/mailbox.twig @@ -33,7 +33,7 @@
  • - + @@ -67,6 +67,7 @@ \\nbad\"}", 400], 'HTTP 400: x bad'], + ]; + } + + #[DataProvider('failedResponses')] + public function testErrorsAreSanitised(array $response, string $error): void + { + $this->client->response = $response; + + $this->assertSame($error, $this->requestError('https://login.example.org/token', false)); + } + + public function testErrorTextIsCutTo200Characters(): void + { + $this->client->response = [json_encode(['error_description' => str_repeat('x', 500)]), 400]; + + $this->assertSame(200, mb_strlen($this->requestError('https://login.example.org/token', false))); + } + + private function requestError(string $url, bool $strict): string + { + try { + $this->client->request($url, $strict, []); + } catch (OAuthException $e) { + return $e->getMessage(); + } + $this->fail('OAuthException expected'); + } +} diff --git a/tests/php/Imapsync/Model/CustomParamsTest.php b/tests/php/Imapsync/Model/CustomParamsTest.php new file mode 100644 index 000000000..924db327b --- /dev/null +++ b/tests/php/Imapsync/Model/CustomParamsTest.php @@ -0,0 +1,146 @@ + false, + 'folder' => true, + 'sslargs1' => '^SSL_verify_mode=[01]$', + 'tilde' => 'a~b', + 'broken' => 5, + ]; + + public function testEmptyInputMeansNoOptions(): void + { + $this->assertSame('[]', CustomParams::toJson(null, self::ALLOWLIST)); + $this->assertSame('[]', CustomParams::toJson('', self::ALLOWLIST)); + $this->assertSame('[]', CustomParams::toJson([], self::ALLOWLIST)); + } + + public function testValidPairsAreNormalisedAndEncoded(): void + { + $json = CustomParams::toJson([ + ['o' => ' --Debug ', 'v' => ''], + ['o' => 'folder', 'v' => "INBOX/Sub\0 äö"], + ['o' => 'sslargs1', 'v' => 'SSL_verify_mode=0'], + ['o' => '', 'v' => ' '], + ], self::ALLOWLIST); + + $this->assertSame([ + ['o' => 'debug', 'v' => ''], + ['o' => 'folder', 'v' => 'INBOX/Sub äö'], + ['o' => 'sslargs1', 'v' => 'SSL_verify_mode=0'], + ], json_decode($json, true)); + $this->assertSame('[{"o":"debug","v":""},{"o":"folder","v":"INBOX/Sub äö"},{"o":"sslargs1","v":"SSL_verify_mode=0"}]', $json); + } + + public function testFlagWithWhitespaceValueIsStoredWithoutValue(): void + { + $this->assertSame('[{"o":"debug","v":""}]', CustomParams::toJson([['o' => 'debug', 'v' => ' ']], self::ALLOWLIST)); + } + + public static function invalidInputs(): array + { + return [ + 'string instead of list' => ['--debug', 'imapsync_custom_params_format'], + 'pair is no array' => [['debug'], 'imapsync_custom_params_format'], + 'option is array' => [[['o' => ['x'], 'v' => '']], 'imapsync_custom_params_format'], + 'value is array' => [[['o' => 'folder', 'v' => ['x']]], 'imapsync_custom_params_format'], + 'unknown option' => [[['o' => 'logfile', 'v' => '/etc/passwd']], ['imapsync_custom_param_invalid', '<b>logfile']], + 'value without option' => [[['o' => '', 'v' => 'x']], ['imapsync_custom_param_invalid', '']], + 'flag with value' => [[['o' => 'debug', 'v' => '1']], ['imapsync_custom_param_value', 'debug']], + 'value option without value' => [[['o' => 'folder', 'v' => ' ']], ['imapsync_custom_param_value', 'folder']], + 'regex mismatch' => [[['o' => 'sslargs1', 'v' => 'SSL_ca_file=/etc/x']], ['imapsync_custom_param_value', 'sslargs1']], + 'regex must match whole value' => [[['o' => 'sslargs1', 'v' => "SSL_verify_mode=1\n"]], ['imapsync_custom_param_value', 'sslargs1']], + 'regex empty value' => [[['o' => 'sslargs1', 'v' => '']], ['imapsync_custom_param_value', 'sslargs1']], + 'malformed rule' => [[['o' => 'broken', 'v' => 'x']], ['imapsync_custom_param_invalid', 'broken']], + 'value too long' => [[['o' => 'folder', 'v' => str_repeat('a', 1001)]], 'imapsync_custom_params_format'], + ]; + } + + #[DataProvider('invalidInputs')] + public function testInvalidInputIsRejected(mixed $input, string|array $msg): void + { + try { + CustomParams::toJson($input, self::ALLOWLIST); + $this->fail('UserError expected'); + } catch (UserError $e) { + $this->assertSame($msg, $e->msg()); + } + } + + public function testRegexDelimiterInRuleIsEscaped(): void + { + $this->assertSame('[{"o":"tilde","v":"a~b"}]', CustomParams::toJson([['o' => 'tilde', 'v' => 'a~b']], self::ALLOWLIST)); + } + + public function testAtMostOneHundredPairs(): void + { + $pairs = array_fill(0, 100, ['o' => 'debug', 'v' => '']); + $this->assertCount(100, json_decode(CustomParams::toJson($pairs, self::ALLOWLIST), true)); + + $this->expectException(UserError::class); + $this->expectExceptionMessage('imapsync_custom_params_format'); + CustomParams::toJson(array_merge($pairs, [['o' => 'debug', 'v' => '']]), self::ALLOWLIST); + } + + public static function storedValues(): array + { + return [ + 'pairs' => ['[{"o":"folder","v":"a\\/b"}]'], + 'NULL' => [null], + 'old command line string' => ['--debug --folder INBOX'], + 'JSON object' => ['{"o":"debug"}'], + 'list of non-pairs' => ['[1,"x",{"v":"no option"}]'], + ]; + } + + #[DataProvider('storedValues')] + public function testStoredValuesPassThroughUnchanged(?string $stored): void + { + $this->assertSame($stored, Job::fromRow(['id' => 1, 'custom_params' => $stored])->toArray()['custom_params']); + } + + public function testAllowlistDropsMalformedEntries(): void + { + $allowlist = CustomParams::allowlist([ + '--Folder' => true, + 'debug' => false, + 'sslargs1' => '^x$', + '1abc' => true, + 'a-b' => true, + 'empty' => '', + 'number' => 1, + ], ['default' => true]); + + $this->assertSame(['folder' => true, 'debug' => false, 'sslargs1' => '^x$'], $allowlist); + } + + public function testAllowlistFallsBackToDefaults(): void + { + $this->assertSame(['default' => true], CustomParams::allowlist([], ['default' => true])); + $this->assertSame(['default' => true], CustomParams::allowlist(['bad name' => true], ['default' => true])); + } + + public function testOldWhitelistFormatIsIgnored(): void + { + $errorLog = ini_set('error_log', '/dev/null'); + try { + $allowlist = CustomParams::allowlist(['whitelist' => ['debug'], 'folder' => true], ['default' => true]); + } finally { + ini_set('error_log', (string)$errorLog); + } + + $this->assertSame(['folder' => true], $allowlist); + } +} diff --git a/tests/php/Imapsync/Service/JobServiceTest.php b/tests/php/Imapsync/Service/JobServiceTest.php new file mode 100644 index 000000000..c2d4d4431 --- /dev/null +++ b/tests/php/Imapsync/Service/JobServiceTest.php @@ -0,0 +1,371 @@ + false, 'folder' => true]; + + private JobRepository&MockObject $jobs; + private SourceService&MockObject $sources; + private TokenRepository&MockObject $tokens; + private SettingsRepository&MockObject $settings; + + protected function setUp(): void + { + $this->jobs = $this->createMock(JobRepository::class); + $this->sources = $this->createMock(SourceService::class); + $this->tokens = $this->createMock(TokenRepository::class); + $this->settings = $this->createMock(SettingsRepository::class); + } + + private function service(Context $context): JobService + { + return new JobService($this->jobs, $this->sources, $this->tokens, $this->settings, $context, self::ALLOWLIST); + } + + private static function source(array $row = []): Source + { + return Source::fromRow($row + ['id' => 5, 'name' => 'src', 'created_by' => 'admin', 'host1' => 'imap.example.org', + 'port1' => 993, 'enc1' => 'SSL', 'auth_type' => 'PLAIN', 'active' => 1]); + } + + private static function storedJob(array $row = [], array $source = []): Job + { + $source += ['source_name' => 'src', 'source_auth_type' => 'PLAIN', 'source_oauth_flow' => 'client_credentials', + 'source_created_by' => 'admin', 'source_active' => 1]; + return Job::fromRow($row + $source + [ + 'id' => 9, 'user2' => 'local@example.org', 'user1' => 'remote@example.net', 'password1' => 'stored-pw', + 'source_id' => 5, 'mins_interval' => 20, 'maxage' => 0, 'timeout1' => 600, 'timeout2' => 600, + 'maxbytespersecond' => '0', 'exclude' => '', 'subfolder2' => '', 'custom_params' => '[{"o":"debug","v":""}]', + 'last_run' => '2026-10-01 10:00:00', 'success' => 1, 'prio' => 3, 'active' => 1, + ]); + } + + private static function jobInput(array $override = []): array + { + return $override + ['username' => 'local@example.org', 'user1' => 'remote@example.net', 'password1' => 'pw', + 'source_id' => 5, 'mins_interval' => 20]; + } + + private function expectUserError(string|array $msg, callable $call): void + { + try { + $call(); + $this->fail('UserError ' . json_encode($msg) . ' expected'); + } catch (UserError $e) { + $this->assertSame($msg, $e->msg()); + } + } + + public function testCreateNeedsAcl(): void + { + $this->jobs->expects($this->never())->method('insert'); + + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('u@example.org', []))->create(self::jobInput())); + } + + public function testOwnerMailboxRules(): void + { + $this->expectUserError('no_user_defined', fn() => $this->service(FakeContext::domainAdmin())->create(self::jobInput(['username' => null]))); + $this->expectUserError('no_user_defined', fn() => $this->service(FakeContext::admin())->create(self::jobInput(['username' => 'not-a-mailbox']))); + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('u@example.org'))->create(self::jobInput())); + } + + public function testCreateStoresJobWithDefaults(): void + { + $this->sources->method('get')->with(5)->willReturn(self::source()); + $this->jobs->expects($this->once())->method('insert')->with($this->callback(function (Job $job): bool { + return $job->user2 === 'u@example.org' && $job->user1 === 'remote@example.net' && $job->password1 === 'pw' + && $job->maxage === 0 && $job->timeout1 === 600 && $job->timeout2 === 600 && $job->maxBytesPerSecond === '0' + && $job->minsInterval === 20 && $job->active === 0 && $job->customParams === '[{"o":"folder","v":"INBOX"}]'; + })); + + $job = $this->service(FakeContext::user('u@example.org'))->create(self::jobInput([ + 'username' => null, 'user1' => ' remote@example.net ', 'maxage' => 'abc', 'timeout1' => 0, 'maxbytespersecond' => 200000000, + 'custom_params' => [['o' => 'folder', 'v' => 'INBOX']], + ])); + + $this->assertSame('u@example.org', $job->user2); + } + + public function testCreateKeepsValidNumbers(): void + { + $this->sources->method('get')->willReturn(self::source()); + $this->jobs->expects($this->once())->method('insert')->with($this->callback( + fn(Job $job): bool => $job->maxage === 30 && $job->timeout1 === 120 && $job->maxBytesPerSecond === '5000' && $job->minsInterval === 43800 + )); + + $this->service(FakeContext::admin())->create(self::jobInput(['maxage' => '30', 'timeout1' => '120', 'maxbytespersecond' => '5000', 'mins_interval' => 43800])); + } + + public static function invalidCreates(): array + { + return [ + 'user1 missing' => [['user1' => ''], 'imapsync_user1_invalid'], + 'user1 array' => [['user1' => ['x']], 'imapsync_user1_invalid'], + 'user1 too long' => [['user1' => str_repeat('u', 256)], 'imapsync_user1_invalid'], + 'password missing' => [['password1' => ''], 'password_empty'], + 'password array' => [['password1' => ['x']], 'password_empty'], + 'custom params string' => [['custom_params' => '--debug'], 'imapsync_custom_params_format'], + 'custom param unknown' => [['custom_params' => [['o' => 'logfile', 'v' => 'x']]], ['imapsync_custom_param_invalid', 'logfile']], + 'exclude array' => [['exclude' => ['x']], 'access_denied'], + 'interval 0' => [['mins_interval' => 0], 'access_denied'], + 'interval too large' => [['mins_interval' => 43801], 'access_denied'], + 'exclude no regex' => [['exclude' => '(unclosed'], 'access_denied'], + 'user1 checked before password' => [['user1' => '', 'password1' => ''], 'imapsync_user1_invalid'], + ]; + } + + #[DataProvider('invalidCreates')] + public function testCreateValidation(array $override, string|array $msg): void + { + $this->sources->method('get')->willReturn(self::source()); + $this->jobs->expects($this->never())->method('insert'); + + $this->expectUserError($msg, fn() => $this->service(FakeContext::admin())->create(self::jobInput($override))); + } + + public function testCreateNeedsVisibleActiveSource(): void + { + $this->sources->method('get')->willReturnOnConsecutiveCalls(null, self::source(['active' => 0])); + $service = $this->service(FakeContext::admin()); + + $this->expectUserError('imapsync_source_unavailable', fn() => $service->create(self::jobInput())); + $this->expectUserError('imapsync_source_unavailable', fn() => $service->create(self::jobInput())); + } + + public function testDuplicateTupleIsRejected(): void + { + $this->sources->method('get')->willReturn(self::source(['name' => ''])); + $this->jobs->method('tupleExists')->with(5, 'local@example.org', 'remote@example.net', null)->willReturn(true); + + $this->expectUserError(['object_exists', '<src> / remote@example.net'], fn() => $this->service(FakeContext::admin())->create(self::jobInput())); + } + + public function testAuthorizationCodeJobNeedsTokenOfOwnerAndRemoteUser(): void + { + $this->sources->method('get')->willReturn(self::source(['auth_type' => 'XOAUTH2', 'oauth_flow' => 'authorization_code'])); + $this->tokens->method('exists')->with(5, 'local@example.org', 'remote@example.net')->willReturnOnConsecutiveCalls(false, true); + $this->jobs->expects($this->once())->method('insert')->with($this->callback(fn(Job $job): bool => $job->password1 === '')); + $service = $this->service(FakeContext::admin()); + + $this->expectUserError('imapsync_source_oauth_connect_required', fn() => $service->create(self::jobInput())); + $service->create(self::jobInput()); + } + + public function testClientCredentialsJobStoresNoPassword(): void + { + $this->sources->method('get')->willReturn(self::source(['auth_type' => 'XOAUTH2', 'oauth_flow' => 'client_credentials'])); + $this->tokens->expects($this->never())->method('exists'); + $this->jobs->expects($this->once())->method('insert')->with($this->callback(fn(Job $job): bool => $job->password1 === '')); + + $this->service(FakeContext::admin())->create(self::jobInput(['password1' => ''])); + } + + public function testUpdateNeedsAccessToTheJob(): void + { + $this->jobs->method('find')->willReturnOnConsecutiveCalls(null, self::storedJob()); + $this->jobs->expects($this->never())->method('update'); + $service = $this->service(FakeContext::user('u@example.org')); + + $this->expectUserError('access_denied', fn() => $service->update(9, [])); + $this->expectUserError('access_denied', fn() => $service->update(9, [])); + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('local@example.org', []))->update(9, [])); + } + + public function testEmptyPasswordKeepsTheStoredOne(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $stored = []; + $this->jobs->method('update')->willReturnCallback(function (Job $job) use (&$stored): void { + $stored[] = $job->password1; + }); + $service = $this->service(FakeContext::user('local@example.org')); + + $service->update(9, ['password1' => '']); + $service->update(9, ['password1' => ['x']]); + $service->update(9, ['password1' => 'new']); + + $this->assertSame(['stored-pw', 'stored-pw', 'new'], $stored); + } + + public function testMissingAttributesKeepTheirValue(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $this->jobs->expects($this->once())->method('update')->with($this->callback(function (Job $job): bool { + return $job->active === 0 && $job->user1 === 'remote@example.net' && $job->minsInterval === 20 && $job->prio === 3 + && $job->customParams === '[{"o":"debug","v":""}]' && $job->lastRun === null && $job->success === 1 + && $job->maxage === 7 && $job->timeout1 === 600; + })); + $this->sources->expects($this->never())->method('get'); + $this->tokens->expects($this->never())->method('deleteIfUnused'); + + $this->service(FakeContext::user('local@example.org'))->update(9, [ + 'active' => '0', 'user1' => '', 'mins_interval' => '', 'prio' => 99, 'last_run' => '', 'maxage' => '7', 'timeout1' => '', + ]); + } + + public function testOnlyAdminsChangePrio(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $this->jobs->expects($this->once())->method('update')->with($this->callback(fn(Job $job): bool => $job->prio === 99)); + + $this->service(FakeContext::admin())->update(9, ['prio' => '99']); + } + + public function testJobOnInvisibleSourceStaysEditable(): void + { + $this->jobs->method('find')->willReturn(self::storedJob([], ['source_created_by' => 'someone-else', 'source_active' => 0])); + $this->sources->expects($this->never())->method('get'); + $this->jobs->expects($this->once())->method('update'); + + $this->service(FakeContext::user('local@example.org'))->update(9, ['active' => 0]); + } + + public function testMovingToAnotherSourceNeedsVisibleActiveSource(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $this->sources->method('get')->with(6)->willReturnOnConsecutiveCalls(null, self::source(['id' => 6, 'active' => 0])); + $service = $this->service(FakeContext::user('local@example.org')); + + $this->expectUserError('imapsync_source_unavailable', fn() => $service->update(9, ['source_id' => 6])); + $this->expectUserError('imapsync_source_unavailable', fn() => $service->update(9, ['source_id' => 6])); + } + + public function testMovingToXOAuth2SourceWipesPasswordAndChecksToken(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $this->sources->method('get')->willReturn(self::source(['id' => 6, 'auth_type' => 'XOAUTH2', 'oauth_flow' => 'authorization_code'])); + $this->tokens->method('exists')->with(6, 'local@example.org', 'remote@example.net')->willReturnOnConsecutiveCalls(false, true); + $this->jobs->expects($this->once())->method('update')->with($this->callback(fn(Job $job): bool => $job->password1 === '' && $job->sourceId === 6)); + $this->tokens->expects($this->once())->method('deleteIfUnused')->with(5, 'local@example.org', 'remote@example.net'); + $service = $this->service(FakeContext::user('local@example.org')); + + $this->expectUserError('imapsync_source_oauth_connect_required', fn() => $service->update(9, ['source_id' => 6])); + $service->update(9, ['source_id' => 6]); + } + + public function testSwitchingFromXOAuth2NeedsPassword(): void + { + $this->jobs->method('find')->willReturn(self::storedJob(['password1' => ''])); + $this->sources->method('get')->willReturn(self::source(['id' => 6])); + + $this->expectUserError('password_empty', fn() => $this->service(FakeContext::user('local@example.org'))->update(9, ['source_id' => 6])); + } + + public function testRemoteUserOfClientCredentialsSourceNeedsSourceAccess(): void + { + $job = self::storedJob(['password1' => ''], ['source_auth_type' => 'XOAUTH2', 'source_oauth_flow' => 'client_credentials', + 'source_created_by' => 'admin']); + $this->jobs->method('find')->willReturn($job); + $this->sources->method('get')->with(5)->willReturnOnConsecutiveCalls(null, self::source()); + $this->sources->method('canEdit')->willReturn(false); + $this->jobs->expects($this->once())->method('update'); + $service = $this->service(FakeContext::user('local@example.org')); + + $this->expectUserError('access_denied', fn() => $service->update(9, ['user1' => 'ceo@example.net'])); + $service->update(9, ['user1' => 'ceo@example.net']); + } + + public function testChangedTupleMustStayUnique(): void + { + $this->jobs->method('find')->willReturn(self::storedJob()); + $this->jobs->method('tupleExists')->with(5, 'local@example.org', 'other@example.net', 9)->willReturn(true); + + $this->expectUserError(['object_exists', 'src / other@example.net'], + fn() => $this->service(FakeContext::user('local@example.org'))->update(9, ['user1' => 'other@example.net'])); + } + + public function testDeleteDropsUnusedToken(): void + { + $this->jobs->method('find')->willReturnOnConsecutiveCalls(self::storedJob(['user2' => 'x@example.org']), self::storedJob()); + $this->jobs->expects($this->once())->method('delete')->with(9); + $this->tokens->expects($this->once())->method('deleteIfUnused')->with(5, 'local@example.org', 'remote@example.net'); + $service = $this->service(FakeContext::user('local@example.org')); + + $this->expectUserError('access_denied', fn() => $service->delete(9)); + $service->delete(9); + } + + public function testAdminsManageOrphanedJobs(): void + { + $this->jobs->method('find')->willReturn(self::storedJob(['user2' => 'deleted@example.org'])); + + $this->assertNotNull($this->service(new FakeContext('admin', 'admin'))->get(9)); + $this->assertNull($this->service(FakeContext::domainAdmin())->get(9)); + } + + public function testJobListsFollowMailboxAccess(): void + { + $this->jobs->method('idsForMailbox')->willReturnMap([['u@example.org', [1, 2]], ['m@example.org', [3]]]); + $this->jobs->method('allIds')->willReturn([3, 1, 2]); + $user = $this->service(FakeContext::user('u@example.org')); + + $this->assertSame([1, 2], $user->idsForMailbox(null)); + $this->assertSame([1, 2], $user->idsForMailbox('not-an-address')); + $this->assertSame([], $user->idsForMailbox('m@example.org')); + $this->assertSame([3], $this->service(FakeContext::domainAdmin(['m@example.org']))->idsForMailbox('m@example.org')); + $this->assertSame([], $this->service(new FakeContext('', null))->idsForMailbox(null)); + $this->assertSame([], $user->allIds()); + $this->assertSame([3, 1, 2], $this->service(FakeContext::admin())->allIds()); + } + + public function testRunNextIsAdminOnly(): void + { + $this->jobs->expects($this->once())->method('moveToFront')->with([4, 2])->willReturn(['running' => [], 'moved' => 2]); + + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::domainAdmin())->runNext([4])); + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::admin())->runNext([])); + $this->assertSame(['running' => [], 'moved' => 2], $this->service(FakeContext::admin())->runNext([4, 2, 4])); + } + + public static function invalidSettings(): array + { + return [ + 'parallel 0' => [['max_parallel' => 0], 'imapsync_max_parallel_invalid'], + 'parallel 33' => [['max_parallel' => 33], 'imapsync_max_parallel_invalid'], + 'parallel text' => [['max_parallel' => 'many', 'max_kb_per_second' => -1], 'imapsync_max_parallel_invalid'], + 'kb negative' => [['max_parallel' => 2, 'max_kb_per_second' => -1], 'imapsync_max_kb_per_second_invalid'], + 'kb too large' => [['max_parallel' => 2, 'max_kb_per_second' => 10485761], 'imapsync_max_kb_per_second_invalid'], + ]; + } + + #[DataProvider('invalidSettings')] + public function testSettingsValidation(array $input, string $msg): void + { + $this->settings->expects($this->never())->method('save'); + + $this->expectUserError($msg, fn() => $this->service(FakeContext::admin())->saveSettings($input)); + } + + public function testSettingsAreStoredInBytes(): void + { + $this->settings->expects($this->exactly(2))->method('save')->with($this->callback( + fn(Settings $s): bool => $s->maxParallel === 4 && in_array($s->maxBytesPerSecond, [0, 2048], true) + )); + $service = $this->service(FakeContext::admin()); + + $this->assertSame(2048, $service->saveSettings(['max_parallel' => '4', 'max_kb_per_second' => '2'])->maxBytesPerSecond); + $this->assertSame(0, $service->saveSettings(['max_parallel' => 4, 'max_kb_per_second' => ''])->maxBytesPerSecond); + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::domainAdmin())->saveSettings(['max_parallel' => 1])); + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::domainAdmin())->getSettings()); + } +} diff --git a/tests/php/Imapsync/Service/OAuthServiceTest.php b/tests/php/Imapsync/Service/OAuthServiceTest.php new file mode 100644 index 000000000..9da896d0e --- /dev/null +++ b/tests/php/Imapsync/Service/OAuthServiceTest.php @@ -0,0 +1,278 @@ +sources = $this->createMock(SourceRepository::class); + $this->tokens = $this->createMock(TokenRepository::class); + $this->client = $this->createMock(OAuthClient::class); + } + + private function service(string $hostname = 'Mail.Example.org '): OAuthService + { + return new OAuthService($this->sources, $this->tokens, $this->client, $hostname); + } + + private static function source(array $row = []): Source + { + return Source::fromRow($row + [ + 'id' => 4, 'name' => 'm365', 'created_by' => 'da', 'host1' => 'outlook.office365.com', 'port1' => 993, + 'auth_type' => 'XOAUTH2', 'oauth_flow' => 'client_credentials', 'active' => 1, + 'oauth_token_endpoint' => 'https://login.example.org/token', 'oauth_authorize_endpoint' => 'https://login.example.org/authorize?tenant=x', + 'oauth_userinfo_endpoint' => 'https://login.example.org/userinfo', + 'oauth_client_id' => 'client', 'oauth_client_secret' => 'secret', 'oauth_scope' => 'scope', + ]); + } + + private static function userToken(array $row = []): OAuthToken + { + return OAuthToken::fromRow($row + ['source_id' => 4, 'owner' => 'local@example.org', 'username' => 'remote@example.net', + 'refresh_token' => 'old-refresh', 'access_token' => 'old-access']); + } + + public function testRedirectUriUsesMailcowHostnameOnly(): void + { + $this->assertSame('https://mail.example.org/syncjob-oauth', $this->service()->redirectUri()); + $this->assertSame('', $this->service('')->redirectUri()); + } + + public function testEndpointsOfAdminSourcesAreNotAddressRestricted(): void + { + $this->sources->method('isSuperAdmin')->willReturnMap([['admin', true], ['da', false]]); + $this->client->expects($this->exactly(3))->method('isAllowed') + ->willReturnCallback(fn(string $url, bool $strict): bool => !$strict); + $service = $this->service(); + + foreach (['admin' => true, 'API' => true, 'da' => false] as $createdBy => $allowed) { + $source = self::source(['created_by' => $createdBy, 'oauth_flow' => 'authorization_code']); + $this->assertSame($allowed, $service->authorizeUrl($source, 'state') !== null, $createdBy); + } + } + + public function testAuthorizeUrl(): void + { + $this->client->method('isAllowed')->willReturnOnConsecutiveCalls(true, false); + $source = self::source(['oauth_flow' => 'authorization_code']); + + $this->assertSame('https://login.example.org/authorize?tenant=x&prompt=select_account&client_id=client&response_type=code' + . '&redirect_uri=https%3A%2F%2Fmail.example.org%2Fsyncjob-oauth&response_mode=query&scope=scope&state=abc', + $this->service()->authorizeUrl($source, 'abc')); + $this->assertNull($this->service()->authorizeUrl($source, 'abc')); + } + + public function testSignInParamsGoToTheAuthorizeUrlOnly(): void + { + $this->client->method('isAllowed')->willReturn(true); + $this->client->expects($this->once())->method('request')->with( + 'https://login.example.org/token', + true, + ['resource' => 'r', 'client_id' => 'client', 'client_secret' => 'secret', 'scope' => 'scope', 'grant_type' => 'client_credentials'] + )->willReturn(['access_token' => 'new']); + $extra = '{"prompt":"consent select_account","access_type":"offline","resource":"r"}'; + + // prompt overrides the default; sign-in params never reach the token endpoint and vice versa + $this->assertSame('https://login.example.org/authorize?tenant=x&prompt=consent+select_account&access_type=offline&client_id=client' + . '&response_type=code&redirect_uri=https%3A%2F%2Fmail.example.org%2Fsyncjob-oauth&response_mode=query&scope=scope&state=abc', + $this->service()->authorizeUrl(self::source(['oauth_flow' => 'authorization_code', 'oauth_extra_params' => $extra]), 'abc')); + $this->assertNull($this->service()->refreshSourceToken(self::source(['oauth_extra_params' => $extra]))); + } + + public function testClientCredentialsRefreshStoresToken(): void + { + $this->client->expects($this->once())->method('request')->with( + 'https://login.example.org/token', + true, + ['resource' => 'r', 'client_id' => 'client', 'client_secret' => 'secret', 'scope' => 'scope', 'grant_type' => 'client_credentials'] + )->willReturn(['access_token' => 'new', 'expires_in' => '120']); + $this->tokens->expects($this->once())->method('storeSourceToken')->with(4, 'new', $this->callback( + fn(int $expires): bool => abs($expires - (time() + 120)) <= 2 + )); + + // Stored extra params never override the client and grant fields + $source = self::source(['oauth_extra_params' => '{"resource":"r","client_id":"evil","grant_type":"password"}']); + $this->assertNull($this->service()->refreshSourceToken($source)); + } + + public function testFailedRefreshStoresTheSanitisedError(): void + { + $this->client->method('request')->willThrowException(new OAuthException('HTTP 401: invalid_client')); + $this->tokens->expects($this->once())->method('storeSourceError')->with(4, 'HTTP 401: invalid_client'); + $this->tokens->expects($this->never())->method('storeSourceToken'); + + $this->assertSame('HTTP 401: invalid_client', $this->service()->refreshSourceToken(self::source())); + } + + public function testTokenResponseWithoutAccessTokenIsMalformed(): void + { + $this->client->method('request')->willReturn(['access_token' => ['x']]); + $this->tokens->expects($this->once())->method('storeSourceError')->with(4, 'malformed token response'); + + $this->assertSame('malformed token response', $this->service()->refreshSourceToken(self::source())); + } + + public function testOnlyClientCredentialsSourcesHaveASourceToken(): void + { + $this->client->expects($this->never())->method('request'); + + $this->assertSame('not refreshable', $this->service()->refreshSourceToken(self::source(['oauth_flow' => 'authorization_code']))); + } + + public function testUserRefreshKeepsRefreshTokenWhenProviderSendsNone(): void + { + $source = self::source(['oauth_flow' => 'authorization_code']); + $this->client->method('request')->willReturnOnConsecutiveCalls( + ['access_token' => 'a1'], + ['access_token' => 'a2', 'refresh_token' => 'r2', 'expires_in' => 60] + ); + $stored = []; + $this->tokens->method('storeUserRefresh')->willReturnCallback(function (OAuthToken $t) use (&$stored): void { + $stored[] = [$t->accessToken, $t->refreshToken, $t->tokenExpires - time()]; + }); + + $this->assertNull($this->service()->refreshUserToken($source, self::userToken())); + $this->assertNull($this->service()->refreshUserToken($source, self::userToken())); + + $this->assertEqualsWithDelta([['a1', 'old-refresh', 3600], ['a2', 'r2', 60]], $stored, 2); + } + + public function testUserRefreshFailureIsStoredOnTheToken(): void + { + $this->client->method('request')->willThrowException(new OAuthException('HTTP 400: invalid_grant')); + $this->tokens->expects($this->once())->method('storeUserError')->with($this->isInstanceOf(OAuthToken::class), 'HTTP 400: invalid_grant'); + + $this->assertSame('HTTP 400: invalid_grant', + $this->service()->refreshUserToken(self::source(['oauth_flow' => 'authorization_code']), self::userToken())); + $this->assertSame('not refreshable', + $this->service()->refreshUserToken(self::source(['oauth_flow' => 'authorization_code']), self::userToken(['refresh_token' => '']))); + } + + public function testRefreshDueAppliesExpiryAndRetryRules(): void + { + // due 20 min ahead, retried 3 min after a failure + $soon = time() + 1100; + $this->tokens->method('refreshableSources')->willReturn([ + self::source(['id' => 1, 'name' => 'no token']), + self::source(['id' => 2, 'name' => 'valid', 'oauth_token_expires' => time() + 1300]), + self::source(['id' => 3, 'name' => 'failed recently', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 180]), + self::source(['id' => 4, 'name' => 'failed long ago', 'oauth_token_expires' => $soon, 'oauth_last_refresh_error' => 'HTTP 500', 'refresh_attempt_age' => 181]), + ]); + $this->tokens->method('deleteUnusedOlderThan')->with(86400)->willReturn(0); + $this->tokens->method('refreshableUserTokens')->willReturn([]); + $this->client->expects($this->exactly(2))->method('request')->willReturnOnConsecutiveCalls( + ['access_token' => 't'], + $this->throwException(new OAuthException('HTTP 401: invalid_client')) + ); + + $this->assertSame([ + ['priority' => 'info', 'message' => "Refreshed token for source 'no token' (id=1)"], + ['priority' => 'warning', 'message' => "Refresh failed for source 'failed long ago' (id=4): HTTP 401: invalid_client"], + ], $this->service()->refreshDue()); + } + + public function testRefreshDueCleansUpAndRefreshesUserTokens(): void + { + $source = self::source(['oauth_flow' => 'authorization_code']); + $this->tokens->method('refreshableSources')->willReturn([]); + $this->tokens->method('deleteUnusedOlderThan')->willReturn(2); + $this->tokens->method('refreshableUserTokens')->willReturn([ + self::userToken(), + self::userToken(['username' => 'fresh@example.net', 'token_expires' => time() + 3600]), + self::userToken(['username' => 'db@example.net']), + self::userToken(['source_id' => 99, 'username' => 'orphan@example.net']), + ]); + $this->sources->method('find')->willReturnMap([[4, $source], [99, null]]); + $this->client->method('request')->willReturn(['access_token' => 'new']); + $this->tokens->method('storeUserRefresh')->willReturnOnConsecutiveCalls(null, $this->throwException(new PDOException('lost'))); + + $this->assertSame([ + ['priority' => 'info', 'message' => 'Removed 2 unused user token(s)'], + ['priority' => 'info', 'message' => "Refreshed user token 'remote@example.net' (mailbox local@example.org) on source 'm365' (id=4)"], + ['priority' => 'warning', 'message' => "Refresh failed for user token 'db@example.net' (mailbox local@example.org) on source 'm365' (id=4): PDOException"], + ['priority' => 'warning', 'message' => "Refresh failed for user token 'orphan@example.net' (mailbox local@example.org) on source '' (id=99): not refreshable"], + ], $this->service()->refreshDue()); + } + + public function testConnectUserTakesIdentityFromIdToken(): void + { + $claims = rtrim(strtr(base64_encode(json_encode(['preferred_username' => 'not an address', 'upn' => 'Remote@Example.NET'])), '+/', '-_'), '='); + $this->client->expects($this->once())->method('request')->with( + 'https://login.example.org/token', + true, + $this->callback(fn(array $post): bool => $post['grant_type'] === 'authorization_code' && $post['code'] === 'c0de' + && $post['redirect_uri'] === 'https://mail.example.org/syncjob-oauth') + )->willReturn(['access_token' => 'acc', 'expires_in' => 100, 'id_token' => 'h.' . $claims . '.s']); + $this->tokens->expects($this->once())->method('saveUserToken')->with($this->callback( + fn(OAuthToken $t): bool => $t->sourceId === 4 && $t->owner === 'local@example.org' && $t->username === 'remote@example.net' + && $t->accessToken === 'acc' && $t->refreshToken === '' + )); + + $this->assertSame('remote@example.net', $this->service()->connectUser(self::source(['oauth_flow' => 'authorization_code']), 'local@example.org', 'c0de')); + } + + public function testConnectUserFallsBackToUserinfo(): void + { + $this->client->method('request')->willReturnOnConsecutiveCalls( + ['access_token' => 'acc', 'refresh_token' => 'ref'], + ['email' => 'remote@example.net'] + ); + $this->tokens->expects($this->once())->method('saveUserToken')->with($this->callback(fn(OAuthToken $t): bool => $t->refreshToken === 'ref')); + + $this->assertSame('remote@example.net', $this->service()->connectUser(self::source(['oauth_flow' => 'authorization_code']), 'local@example.org', 'c')); + } + + public function testConnectUserErrors(): void + { + $source = self::source(['oauth_flow' => 'authorization_code']); + $this->client->method('request')->willReturnOnConsecutiveCalls( + $this->throwException(new OAuthException('HTTP 400: invalid_grant')), + ['access_token' => 'acc'], + $this->throwException(new OAuthException('HTTP 401')), + ['access_token' => 'acc', 'id_token' => 'not.a.jwt.at.all'] + ); + $this->tokens->expects($this->never())->method('saveUserToken'); + + $this->assertUserError(['token_failed', 'token exchange failed for source 4: HTTP 400: invalid_grant'], fn() => $this->service()->connectUser($source, 'o', 'c')); + $this->assertUserError('no_identity', fn() => $this->service()->connectUser($source, 'o', 'c')); + $this->assertUserError('no_identity', fn() => $this->service()->connectUser(self::source(['oauth_flow' => 'authorization_code', 'oauth_userinfo_endpoint' => null]), 'o', 'c')); + } + + public function testConnectUserReportsStorageFailure(): void + { + $this->client->method('request')->willReturnOnConsecutiveCalls(['access_token' => 'acc'], ['email' => 'r@example.net']); + $this->tokens->method('saveUserToken')->willThrowException(new PDOException('duplicate')); + + $this->assertUserError(['token_failed', 'storing token failed for source 4: duplicate'], + fn() => $this->service()->connectUser(self::source(['oauth_flow' => 'authorization_code']), 'o', 'c')); + } + + private function assertUserError(string|array $msg, callable $call): void + { + try { + $call(); + $this->fail('UserError expected'); + } catch (UserError $e) { + $this->assertSame($msg, $e->msg()); + } + } +} diff --git a/tests/php/Imapsync/Service/SourceServiceTest.php b/tests/php/Imapsync/Service/SourceServiceTest.php new file mode 100644 index 000000000..d14d26fa7 --- /dev/null +++ b/tests/php/Imapsync/Service/SourceServiceTest.php @@ -0,0 +1,523 @@ +repo = $this->createMock(SourceRepository::class); + $this->tokens = $this->createMock(TokenRepository::class); + $this->client = $this->createMock(OAuthClient::class); + $this->client->method('isAllowed')->willReturn(true); + } + + /** SourceService with the real OAuthService (settings rules); only the HTTP client and the repositories are mocked */ + private function service(Context $context, ?OAuthClient $client = null): SourceService + { + return new SourceService($this->repo, $this->tokens, + new OAuthService($this->repo, $this->tokens, $client ?? $this->client, 'mail.example.org'), $context); + } + + private static function source(array $row = []): Source + { + return Source::fromRow($row + [ + 'id' => 7, + 'name' => 'src', + 'created_by' => 'owner@example.org', + 'scope' => 'user', + 'host1' => 'imap.example.org', + 'port1' => 993, + 'enc1' => 'SSL', + 'auth_type' => 'PLAIN', + 'active' => 1, + ]); + } + + private static function oauthSource(array $row = []): Source + { + return self::source($row + [ + 'created_by' => 'da', + 'auth_type' => 'XOAUTH2', + 'oauth_flow' => 'client_credentials', + 'oauth_token_endpoint' => 'https://login.example.org/token', + 'oauth_client_id' => 'client', + 'oauth_client_secret' => 'stored-secret', + 'oauth_scope' => 'https://outlook.office365.com/.default', + 'oauth_access_token' => 'access', + ]); + } + + private function expectUserError(string|array $msg, callable $call): void + { + try { + $call(); + $this->fail('UserError ' . json_encode($msg) . ' expected'); + } catch (UserError $e) { + $this->assertSame($msg, $e->msg()); + } + } + + public static function editMatrix(): array + { + $other = ['other@example.org']; + return [ + 'admin edits everything' => [FakeContext::admin(), 'x@example.org', true], + 'admin edits handed-over sources' => [FakeContext::admin(), '', true], + 'access_all_exception' => [new FakeContext('user', 'u@example.org', [], true), 'x@example.org', true], + 'user edits own' => [FakeContext::user('u@example.org'), 'u@example.org', true], + 'user not foreign' => [FakeContext::user('u@example.org'), 'x@example.org', false], + 'user not handed-over' => [FakeContext::user('u@example.org'), '', false], + 'domain admin edits own' => [FakeContext::domainAdmin(), 'da', true], + 'domain admin edits his mailboxes' => [FakeContext::domainAdmin($other), 'other@example.org', true], + 'domain admin not foreign mailboxes' => [FakeContext::domainAdmin($other), 'x@example.org', false], + 'domain admin not handed-over' => [FakeContext::domainAdmin(['']), '', false], + 'missing username is no creator' => [new FakeContext('user', null), '', false], + ]; + } + + #[DataProvider('editMatrix')] + public function testEditPermissionMatrix(Context $context, string $createdBy, bool $canEdit): void + { + $this->assertSame($canEdit, $this->service($context)->canEdit(self::source(['created_by' => $createdBy]))); + } + + public function testWithoutAclNothingIsVisibleExceptForAdmins(): void + { + $this->repo->expects($this->once())->method('findVisible')->willReturn(self::source()); + $this->repo->expects($this->once())->method('visibleIds')->willReturn([7]); + + $this->assertNull($this->service(FakeContext::user('u@example.org', []))->get(7)); + $this->assertSame([], $this->service(FakeContext::domainAdmin([], [], []))->visibleIds()); + $this->assertNotNull($this->service(FakeContext::admin([]))->get(7)); + $this->assertSame([7], $this->service(FakeContext::admin([]))->visibleIds()); + } + + public static function noEditors(): array + { + return [ + 'not logged in' => [new FakeContext('', null)], + 'system' => [Context::system()], + 'user without ACL' => [FakeContext::user('u@example.org', [])], + 'domain admin without ACL' => [FakeContext::domainAdmin([], [], ['syncjobs' => '0'])], + ]; + } + + #[DataProvider('noEditors')] + public function testCreateNeedsAnEditorWithAcl(Context $context): void + { + $this->repo->expects($this->never())->method('insert'); + + $this->expectUserError('access_denied', fn() => $this->service($context)->create(['name' => 'a', 'host' => 'h.example.org', 'port' => 993])); + } + + public function testUserSourceIsAlwaysPrivate(): void + { + $this->repo->expects($this->once())->method('insert')->with($this->callback(function (Source $s): bool { + return $s->scope === 'user' && $s->domains === [] && $s->users === [] && $s->createdBy === 'u@example.org' + && $s->host === 'imap.example.org' && $s->port === 993 && $s->enc === 'TLS' && $s->active === 1; + })); + + $source = $this->service(FakeContext::user('u@example.org'))->create([ + 'name' => ' Mine ', 'host' => 'IMAP.example.org', 'port' => '993', 'scope' => 'all', 'domains' => ['example.org'], + ]); + + $this->assertSame('Mine', $source->name); + } + + public function testUsersMayNotCreateXOAuth2Sources(): void + { + $this->repo->expects($this->never())->method('insert'); + + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::user())->create($this->oauthInput())); + } + + public function testDomainAdminScopeRules(): void + { + $service = $this->service(FakeContext::domainAdmin(['m@example.org'], ['example.org'])); + $input = ['name' => 'a', 'host' => 'imap.example.org', 'port' => 993]; + + $this->expectUserError('access_denied', fn() => $service->create($input + ['scope' => 'all'])); + $this->expectUserError('access_denied', fn() => $service->create($input + ['scope' => 'everyone'])); + $this->expectUserError('access_denied', fn() => $service->create($input + ['scope' => 'domain', 'domains' => ['foreign.org']])); + $this->expectUserError('access_denied', fn() => $service->create($input + ['scope' => 'user', 'users' => ['x@foreign.org']])); + $this->expectUserError('imapsync_source_scope_empty', fn() => $service->create($input + ['scope' => 'domain', 'domains' => [' ', ['x']]])); + } + + public function testDomainScopeTargetsAreCleaned(): void + { + $this->repo->expects($this->once())->method('insert')->with($this->callback( + fn(Source $s): bool => $s->scope === 'domain' && $s->domains === ['example.org'] && $s->users === [] + )); + + $this->service(FakeContext::domainAdmin([], ['example.org']))->create([ + 'name' => 'a', 'host' => 'imap.example.org', 'port' => 993, + 'scope' => 'domain', 'domains' => [' Example.org', 'example.org', ''], 'users' => ['ignored@example.org'], + ]); + } + + public function testClientCredentialsSourcesAreNeverShared(): void + { + $this->repo->expects($this->once())->method('insert')->with($this->callback( + fn(Source $s): bool => $s->scope === 'user' && $s->domains === [] && $s->usesClientCredentials() + )); + + $this->service(FakeContext::admin())->create($this->oauthInput() + ['scope' => 'all']); + } + + public static function invalidBasics(): array + { + $valid = ['name' => 'a', 'description' => '', 'host' => 'imap.example.org', 'port' => 993, 'enc' => 'TLS', 'auth_type' => 'PLAIN']; + return [ + 'name missing' => [['name' => ''] + $valid, 'imapsync_source_name_invalid'], + 'name too long' => [['name' => str_repeat('n', 101)] + $valid, 'imapsync_source_name_invalid'], + 'name is array' => [['name' => ['x']] + $valid, 'imapsync_source_name_invalid'], + 'description too long' => [['description' => str_repeat('d', 256)] + $valid, 'imapsync_source_description_invalid'], + 'host invalid' => [['host' => 'imap_server.lan'] + $valid, 'imapsync_source_host_invalid'], + 'host too long' => [['host' => str_repeat('a.', 127) . 'org'] + $valid, 'imapsync_source_host_invalid'], + 'port 0' => [['port' => 0] + $valid, 'imapsync_source_port_invalid'], + 'port too high' => [['port' => 65536] + $valid, 'imapsync_source_port_invalid'], + 'enc unknown' => [['enc' => 'STARTTLS'] + $valid, 'imapsync_source_enc_invalid'], + 'auth unknown' => [['auth_type' => 'GSSAPI'] + $valid, 'imapsync_source_auth_invalid'], + 'name checked first' => [['name' => '', 'host' => '', 'port' => 0] + $valid, 'imapsync_source_name_invalid'], + ]; + } + + #[DataProvider('invalidBasics')] + public function testCreateValidatesBasics(array $input, string $msg): void + { + $this->expectUserError($msg, fn() => $this->service(FakeContext::admin())->create($input)); + } + + public function testIpAddressAndLegacyAliasesAreAccepted(): void + { + $this->repo->expects($this->once())->method('insert')->with($this->callback( + fn(Source $s): bool => $s->host === '192.0.2.10' && $s->port === 143 && $s->enc === 'PLAIN' + )); + + $this->service(FakeContext::admin())->create(['name' => 'a', 'host1' => '192.0.2.10', 'port1' => 143, 'enc1' => 'PLAIN']); + } + + public function testNameMustBeUniquePerCreator(): void + { + $this->repo->method('nameExists')->with('', 'admin', null)->willReturn(true); + $this->repo->expects($this->never())->method('insert'); + + $this->expectUserError(['object_exists', '<b>'], fn() => $this->service(FakeContext::admin())->create( + ['name' => '', 'host' => 'imap.example.org', 'port' => 993] + )); + } + + public static function invalidOAuth(): array + { + return [ + 'secret missing' => [['oauth_client_secret' => ''], 'imapsync_source_oauth_required'], + 'scope missing' => [['oauth_scope' => ''], 'imapsync_source_oauth_required'], + 'endpoint is array' => [['oauth_token_endpoint' => ['x']], 'imapsync_source_oauth_invalid'], + 'secret is array' => [['oauth_client_secret' => ['x']], 'imapsync_source_oauth_invalid'], + 'client id too long' => [['oauth_client_id' => str_repeat('c', 501)], 'imapsync_source_oauth_invalid'], + 'extra params list' => [['oauth_extra_params' => '[1,2]'], 'imapsync_source_oauth_extra_invalid'], + 'extra params nested' => [['oauth_extra_params' => ['a' => ['b' => 1]]], 'imapsync_source_oauth_extra_invalid'], + 'extra params no JSON' => [['oauth_extra_params' => '{nope'], 'imapsync_source_oauth_extra_invalid'], + 'extra params too long' => [['oauth_extra_params' => ['a' => str_repeat('x', 4000)]], 'imapsync_source_oauth_extra_invalid'], + ]; + } + + #[DataProvider('invalidOAuth')] + public function testOAuthAttributesAreValidated(array $override, string $msg): void + { + $this->expectUserError($msg, fn() => $this->service(FakeContext::admin())->create($override + $this->oauthInput())); + } + + public function testExtraParamsAreAcceptedAsArrayOrString(): void + { + $stored = []; + $this->repo->method('insert')->willReturnCallback(function (Source $s) use (&$stored): int { + $stored[] = $s->extraParams; + return 1; + }); + $service = $this->service(FakeContext::admin()); + + $service->create(['oauth_extra_params' => ['resource' => 'https://x/', 'n' => 1]] + $this->oauthInput()); + $service->create(['oauth_extra_params' => '{"resource":"https://x/"}'] + $this->oauthInput()); + $service->create(['oauth_extra_params' => ' '] + $this->oauthInput()); + + $this->assertSame(['{"resource":"https://x/","n":1}', '{"resource":"https://x/"}', null], $stored); + } + + public function testEndpointsMustPassThePolicyOfTheCreator(): void + { + // Sources of a domain admin ('da', no superadmin) get the strict address policy for every endpoint + $client = $this->createMock(OAuthClient::class); + $client->expects($this->exactly(3))->method('isAllowed') + ->willReturnCallback(fn(string $url, bool $strict): bool => $strict && $url !== 'https://internal/userinfo'); + $this->repo->method('isSuperAdmin')->with('da')->willReturn(false); + $service = $this->service(FakeContext::domainAdmin(), $client); + + $this->expectUserError('imapsync_source_oauth_endpoint_invalid', fn() => $service->create([ + 'oauth_flow' => 'authorization_code', + 'oauth_authorize_endpoint' => 'https://login.example.org/authorize', + 'oauth_userinfo_endpoint' => 'https://internal/userinfo', + ] + $this->oauthInput())); + } + + public function testUpdateNeedsVisibleEditableSource(): void + { + $this->repo->method('findVisible')->willReturnOnConsecutiveCalls(null, self::source(['created_by' => 'x@example.org'])); + $this->repo->expects($this->never())->method('update'); + $service = $this->service(FakeContext::user('u@example.org')); + + $this->expectUserError('access_denied', fn() => $service->update(7, ['name' => 'b'])); + $this->expectUserError('access_denied', fn() => $service->update(7, ['name' => 'b'])); + } + + public function testUsersMayNotSwitchToXOAuth2(): void + { + $this->repo->method('findVisible')->willReturn(self::source(['created_by' => 'u@example.org'])); + $this->repo->expects($this->never())->method('update'); + + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('u@example.org'))->update(7, $this->oauthInput())); + } + + public function testUnchangedLegacyValuesAreNotValidatedAgain(): void + { + $this->repo->method('findVisible')->willReturn(self::source(['host1' => 'imap_server.lan', 'enc1' => null])); + $this->repo->expects($this->once())->method('update')->with( + $this->callback(fn(Source $s): bool => $s->name === 'renamed' && $s->host === 'imap_server.lan' && $s->enc === null), + false, + false + ); + + $this->service(FakeContext::admin())->update(7, ['name' => 'renamed', 'host' => 'IMAP_SERVER.lan']); + } + + public function testRenameMustStayUniqueForTheCreator(): void + { + $this->repo->method('findVisible')->willReturn(self::source()); + $this->repo->method('nameExists')->with('taken', 'owner@example.org', 7)->willReturn(true); + + $this->expectUserError(['object_exists', 'taken'], fn() => $this->service(FakeContext::admin())->update(7, ['name' => 'taken'])); + } + + public function testEmptySecretKeepsStoredOneAndTokensSurviveUnrelatedChanges(): void + { + $this->repo->method('findVisible')->willReturn(self::oauthSource()); + $this->repo->expects($this->once())->method('update')->with( + $this->callback(fn(Source $s): bool => $s->clientSecret === 'stored-secret' && $s->oauthScope === 'new-scope'), + true, + false + ); + + $this->service(FakeContext::domainAdmin())->update(7, ['oauth_client_secret' => '', 'oauth_scope' => 'new-scope']); + } + + public static function connectionChanges(): array + { + return [ + 'token endpoint' => [['oauth_token_endpoint' => 'https://other.example.org/token']], + 'client id' => [['oauth_client_id' => 'other']], + 'host' => [['host' => 'other.example.org']], + 'port' => [['port' => 143]], + 'encryption' => [['enc' => 'TLS']], + 'flow' => [['oauth_flow' => 'authorization_code', 'oauth_authorize_endpoint' => 'https://login.example.org/a']], + 'auth type' => [['auth_type' => 'PLAIN']], + ]; + } + + #[DataProvider('connectionChanges')] + public function testTokensAreResetWhenTheConnectionChanges(array $input): void + { + $this->repo->method('findVisible')->willReturn(self::oauthSource()); + $this->repo->expects($this->once())->method('update')->with($this->anything(), $this->anything(), true); + + $this->service(FakeContext::domainAdmin())->update(7, $input); + } + + public function testTargetsAreOnlyRewrittenWhenScopeIsSent(): void + { + $this->repo->method('findVisible')->willReturn(self::source(['scope' => 'domain'])); + $calls = []; + $this->repo->method('update')->willReturnCallback(function (Source $s, bool $targets) use (&$calls): void { + $calls[] = [$s->scope, $targets]; + }); + $service = $this->service(FakeContext::admin()); + + $service->update(7, ['name' => 'x']); + $service->update(7, ['scope' => 'all']); + + $this->assertSame([['domain', false], ['all', true]], $calls); + } + + public function testApiArrayNeverContainsSecrets(): void + { + $source = self::oauthSource(['oauth_extra_params' => '{"a":"b"}']); + + $editor = $this->service(FakeContext::admin())->toArray($source); + $viewer = $this->service(FakeContext::user('u@example.org'))->toArray($source); + + foreach ([$editor, $viewer] as $data) { + $this->assertArrayNotHasKey('oauth_client_secret', $data); + $this->assertArrayNotHasKey('oauth_access_token', $data); + $this->assertStringNotContainsString('stored-secret', json_encode($data)); + $this->assertTrue($data['has_client_secret']); + $this->assertTrue($data['has_access_token']); + $this->assertArrayNotHasKey('oauth_redirect_uri', $data); + } + $this->assertTrue($editor['can_edit']); + $this->assertSame('{"a":"b"}', $editor['oauth_extra_params']); + $this->assertFalse($viewer['can_edit']); + $this->assertNull($viewer['oauth_extra_params']); + } + + public function testApiArrayOfAuthorizationCodeSourceHasRedirectUri(): void + { + $data = $this->service(FakeContext::admin())->toArray(self::oauthSource(['oauth_flow' => 'authorization_code'])); + + $this->assertSame('https://mail.example.org/syncjob-oauth', $data['oauth_redirect_uri']); + $this->assertSame(['host' => 'imap.example.org', 'port' => 993, 'enc' => 'SSL'], + array_intersect_key($data, ['host' => 1, 'port' => 1, 'enc' => 1])); + } + + public function testDeleteRefusesSourcesInUse(): void + { + $this->repo->method('findVisible')->willReturn(self::source()); + $this->repo->method('isInUse')->willReturnOnConsecutiveCalls(true, false); + $this->repo->expects($this->once())->method('delete')->with(7); + $service = $this->service(FakeContext::admin()); + + $this->expectUserError(['imapsync_source_in_use', 'src'], fn() => $service->delete(7)); + $this->assertSame('src', $service->delete(7)->name); + } + + public function testRefreshTokenRules(): void + { + $this->repo->method('findVisible')->willReturnOnConsecutiveCalls( + self::oauthSource(['oauth_flow' => 'authorization_code']), + self::oauthSource(), + self::oauthSource() + ); + $this->client->method('request')->willReturnOnConsecutiveCalls( + $this->throwException(new OAuthException('HTTP 401: invalid_client')), + ['access_token' => 'new'] + ); + $service = $this->service(FakeContext::admin()); + + $this->expectUserError('access_denied', fn() => $this->service(FakeContext::admin([]))->refreshToken(7)); + $this->expectUserError('imapsync_source_refresh_unsupported', fn() => $service->refreshToken(7)); + $this->expectUserError(['imapsync_source_token_refresh_failed', 'src (HTTP 401: invalid_client)'], fn() => $service->refreshToken(7)); + $this->assertSame('src', $service->refreshToken(7)->name); + } + + public function testSavedClientCredentialsSourcesFetchTheirFirstToken(): void + { + $this->repo->method('findVisible')->willReturnOnConsecutiveCalls(self::oauthSource(), self::oauthSource(['active' => 0])); + $this->client->expects($this->exactly(2))->method('request')->willReturnOnConsecutiveCalls( + ['access_token' => 'first', 'expires_in' => 3600], + $this->throwException(new OAuthException('HTTP 401: invalid_client')) + ); + $this->tokens->expects($this->once())->method('storeSourceToken'); + $this->tokens->expects($this->once())->method('storeSourceError')->with(7, 'HTTP 401: invalid_client'); + $service = $this->service(FakeContext::admin()); + + $this->assertNull($service->create($this->oauthInput())->lastRefreshError); + $this->assertSame('HTTP 401: invalid_client', $service->update(7, ['oauth_scope' => 'new-scope'])->lastRefreshError); + // inactive sources get no token + $service->update(7, ['oauth_scope' => 'other-scope']); + } + + public function testConnectableSourceIsActiveAuthorizationCode(): void + { + $this->repo->method('findVisible')->willReturnOnConsecutiveCalls( + self::oauthSource(['oauth_flow' => 'authorization_code']), + self::oauthSource(['oauth_flow' => 'authorization_code', 'active' => 0]), + self::oauthSource() + ); + $service = $this->service(FakeContext::user()); + + $this->assertNotNull($service->findConnectable(7)); + $this->assertNull($service->findConnectable(7)); + $this->assertNull($service->findConnectable(7)); + } + + public function testAccountRenameReleasesLeftoversAndMovesEverything(): void + { + $this->repo->method('listCreatedBy')->with('new@example.org')->willReturn([ + self::source(['id' => 1, 'name' => 'unused']), + self::source(['id' => 2, 'name' => 'used']), + ]); + $this->repo->method('isInUse')->willReturnMap([[1, false], [2, true]]); + $this->repo->method('nameExists')->with('used', '')->willReturn(true); + $this->repo->expects($this->once())->method('deleteWithChildren')->with(1); + $this->repo->expects($this->once())->method('handOver')->with(2, 'used (#2)'); + $this->repo->expects($this->once())->method('renameAccount')->with('old@example.org', 'new@example.org'); + $this->tokens->expects($this->once())->method('renameOwner')->with('old@example.org', 'new@example.org'); + + $this->service(Context::system())->onAccountRenamed('old@example.org', 'new@example.org'); + } + + public function testAccountRenameToItselfDoesNothing(): void + { + $this->repo->expects($this->never())->method('listCreatedBy'); + $this->tokens->expects($this->never())->method('renameOwner'); + + $this->service(Context::system())->onAccountRenamed('a@example.org', 'a@example.org'); + $this->service(Context::system())->onAccountDeleted(''); + } + + public function testAccountDeleteRemovesTokensAndTargets(): void + { + $this->repo->method('listCreatedBy')->with('old@example.org')->willReturn([self::source(['id' => 3, 'name' => str_repeat('n', 100)])]); + $this->repo->method('isInUse')->willReturn(true); + $this->repo->method('nameExists')->willReturn(true); + $this->repo->expects($this->once())->method('handOver')->with(3, str_repeat('n', 80) . ' (#3)'); + $this->tokens->expects($this->once())->method('deleteByOwner')->with('old@example.org'); + $this->repo->expects($this->once())->method('deleteUserTarget')->with('old@example.org'); + + $this->service(Context::system())->onAccountDeleted('old@example.org'); + } + + public function testAccountCascadeLetsDatabaseErrorsThrough(): void + { + $this->repo->method('listCreatedBy')->willThrowException(new PDOException('gone')); + + $this->expectException(PDOException::class); + $this->service(Context::system())->onAccountDeleted('old@example.org'); + } + + private function oauthInput(): array + { + return [ + 'name' => 'oauth', + 'host' => 'outlook.office365.com', + 'port' => 993, + 'enc' => 'SSL', + 'auth_type' => 'XOAUTH2', + 'oauth_flow' => 'client_credentials', + 'oauth_token_endpoint' => 'https://login.example.org/token', + 'oauth_client_id' => 'client', + 'oauth_client_secret' => 'secret', + 'oauth_scope' => 'scope', + ]; + } +} diff --git a/tests/php/Support/FakeContext.php b/tests/php/Support/FakeContext.php new file mode 100644 index 000000000..8a868a63a --- /dev/null +++ b/tests/php/Support/FakeContext.php @@ -0,0 +1,56 @@ + '1'], bool $accessAll = false, + array $mailboxes = [], array $domains = []) + { + parent::__construct($role, $username, $acl, $accessAll); + $this->mailboxes = $mailboxes; + $this->domains = $domains; + } + + public static function admin(array $acl = ['syncjobs' => '1']): self + { + return new self('admin', 'admin', $acl, false, ['*'], ['*']); + } + + public static function domainAdmin(array $mailboxes = [], array $domains = [], array $acl = ['syncjobs' => '1']): self + { + return new self('domainadmin', 'da', $acl, false, $mailboxes, $domains); + } + + public static function user(string $username = 'user@example.org', array $acl = ['syncjobs' => '1']): self + { + return new self('user', $username, $acl); + } + + public function canAccessMailbox(string $mailbox): bool + { + if ($this->accessAll) { + return true; + } + if (!in_array($this->role, ['admin', 'domainadmin', 'user'], true) || $this->username === null) { + return false; + } + return $mailbox === $this->username || in_array($mailbox, $this->mailboxes, true) || in_array('*', $this->mailboxes, true); + } + + public function canAccessDomain(string $domain): bool + { + return $this->accessAll || in_array($domain, $this->domains, true) || in_array('*', $this->domains, true); + } +} diff --git a/tests/php/Support/FakeOAuthClient.php b/tests/php/Support/FakeOAuthClient.php new file mode 100644 index 000000000..5c0e16a05 --- /dev/null +++ b/tests/php/Support/FakeOAuthClient.php @@ -0,0 +1,35 @@ + host => addresses */ + public array $addresses = []; + /** @var array{0: string|false, 1: int} */ + public array $response = ['{}', 200]; + /** @var array[] curl options of every send() */ + public array $sent = []; + /** @var string[] hosts passed to resolve() */ + public array $resolved = []; + + protected function resolve(string $host): array + { + $this->resolved[] = $host; + return $this->addresses[$host] ?? []; + } + + protected function send(array $options): array + { + $this->sent[] = $options; + return $this->response; + } +} diff --git a/tests/php/Support/LegacyAclCalls.php b/tests/php/Support/LegacyAclCalls.php new file mode 100644 index 000000000..bdee3aef0 --- /dev/null +++ b/tests/php/Support/LegacyAclCalls.php @@ -0,0 +1,13 @@ +=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.57", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "36b75bf56a8535623f5b3e24b27ea1823759da1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/36b75bf56a8535623f5b3e24b27ea1823759da1a", + "reference": "36b75bf56a8535623f5b3e24b27ea1823759da1a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.57" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-10-05T05:47:54+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.2.29" + }, + "plugin-api-version": "2.9.0" +} diff --git a/tests/php/phpunit.xml.dist b/tests/php/phpunit.xml.dist new file mode 100644 index 000000000..27aa18324 --- /dev/null +++ b/tests/php/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + . + vendor + + + + + ../../data/web/inc/lib/mailcow + + +