From 82c209c8e71b9473af38085588bda376ab370f98 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Mon, 23 Feb 2009 00:16:41 -0700 Subject: [PATCH] Import ingo from CVS HEAD. --- .gitignore | 1 + ingo/LICENSE | 49 + ingo/README | 85 + ingo/blacklist.php | 108 + ingo/config/.htaccess | 1 + ingo/config/backends.php.dist | 327 +++ ingo/config/conf.xml | 70 + ingo/config/fields.php.dist | 108 + ingo/config/hooks.php.dist | 24 + ingo/config/prefs.php.dist | 110 + ingo/docs/CHANGES | 394 +++ ingo/docs/CREDITS | 82 + ingo/docs/INSTALL | 211 ++ ingo/docs/RELEASE_NOTES | 48 + ingo/docs/TODO | 10 + ingo/docs/UPGRADING | 89 + ingo/filters.php | 322 +++ ingo/forward.php | 105 + ingo/index.php | 26 + ingo/lib/Block/overview.php | 111 + ingo/lib/Driver.php | 82 + ingo/lib/Driver/ldap.php | 263 ++ ingo/lib/Driver/null.php | 24 + ingo/lib/Driver/sivtest.php | 210 ++ ingo/lib/Driver/timsieved.php | 124 + ingo/lib/Driver/vfs.php | 143 + ingo/lib/Ingo.php | 424 +++ ingo/lib/Script.php | 339 +++ ingo/lib/Script/imap.php | 463 +++ ingo/lib/Script/imap/live.php | 122 + ingo/lib/Script/imap/mock.php | 117 + ingo/lib/Script/maildrop.php | 761 +++++ ingo/lib/Script/procmail.php | 802 ++++++ ingo/lib/Script/sieve.php | 2976 ++++++++++++++++++++ ingo/lib/Session.php | 71 + ingo/lib/Storage.php | 1029 +++++++ ingo/lib/Storage/mock.php | 55 + ingo/lib/Storage/prefs.php | 183 ++ ingo/lib/Storage/sql.php | 718 +++++ ingo/lib/Template.php | 511 ++++ ingo/lib/UI/VarRenderer/ingo.php | 20 + ingo/lib/api.php | 288 ++ ingo/lib/base.php | 104 + ingo/lib/tests/MaildropTest.php | 121 + ingo/lib/tests/ProcmailTest.php | 186 ++ ingo/lib/tests/ScriptTest.php | 339 +++ ingo/lib/tests/SieveTest.php | 137 + ingo/lib/tests/TestBase.php | 51 + ingo/lib/tests/_data/from_spammer | 40 + ingo/lib/tests/_data/not_from_spammer | 40 + ingo/lib/version.php | 1 + ingo/locale/ca_ES/LC_MESSAGES/ingo.mo | Bin 0 -> 149951 bytes ingo/locale/ca_ES/help.xml | 286 ++ ingo/locale/cs_CZ/LC_MESSAGES/ingo.mo | Bin 0 -> 146857 bytes ingo/locale/da_DK/LC_MESSAGES/ingo.mo | Bin 0 -> 138209 bytes ingo/locale/de_DE/LC_MESSAGES/ingo.mo | Bin 0 -> 173292 bytes ingo/locale/de_DE/help.xml | 294 ++ ingo/locale/el_GR/LC_MESSAGES/ingo.mo | Bin 0 -> 136296 bytes ingo/locale/en_US/help.xml | 267 ++ ingo/locale/es_ES/LC_MESSAGES/ingo.mo | Bin 0 -> 165652 bytes ingo/locale/es_ES/help.xml | 286 ++ ingo/locale/et_EE/LC_MESSAGES/ingo.mo | Bin 0 -> 149706 bytes ingo/locale/eu_ES/LC_MESSAGES/ingo.mo | Bin 0 -> 154446 bytes ingo/locale/eu_ES/help.xml | 53 + ingo/locale/fi_FI/LC_MESSAGES/ingo.mo | Bin 0 -> 161690 bytes ingo/locale/fi_FI/help.xml | 275 ++ ingo/locale/fr_FR/LC_MESSAGES/ingo.mo | Bin 0 -> 163959 bytes ingo/locale/fr_FR/help.xml | 361 +++ ingo/locale/hu_HU/LC_MESSAGES/ingo.mo | Bin 0 -> 164281 bytes ingo/locale/it_IT/LC_MESSAGES/ingo.mo | Bin 0 -> 162113 bytes ingo/locale/ja_JP/LC_MESSAGES/ingo.mo | Bin 0 -> 149348 bytes ingo/locale/ko_KR/LC_MESSAGES/ingo.mo | Bin 0 -> 27137 bytes ingo/locale/lt_LT/LC_MESSAGES/ingo.mo | Bin 0 -> 155232 bytes ingo/locale/lv_LV/LC_MESSAGES/ingo.mo | Bin 0 -> 26651 bytes ingo/locale/lv_LV/help.xml | 1262 +++++++++ ingo/locale/nb_NO/LC_MESSAGES/ingo.mo | Bin 0 -> 93448 bytes ingo/locale/nl_NL/LC_MESSAGES/ingo.mo | Bin 0 -> 158529 bytes ingo/locale/pl_PL/LC_MESSAGES/ingo.mo | Bin 0 -> 80472 bytes ingo/locale/pt_BR/LC_MESSAGES/ingo.mo | Bin 0 -> 164389 bytes ingo/locale/pt_PT/LC_MESSAGES/ingo.mo | Bin 0 -> 156474 bytes ingo/locale/ro_RO/LC_MESSAGES/ingo.mo | Bin 0 -> 22417 bytes ingo/locale/ru_RU/LC_MESSAGES/ingo.mo | Bin 0 -> 24224 bytes ingo/locale/sk_SK/LC_MESSAGES/ingo.mo | Bin 0 -> 160849 bytes ingo/locale/sl_SI/LC_MESSAGES/ingo.mo | Bin 0 -> 142183 bytes ingo/locale/sv_SE/LC_MESSAGES/ingo.mo | Bin 0 -> 97456 bytes ingo/locale/tr_TR/LC_MESSAGES/ingo.mo | Bin 0 -> 156765 bytes ingo/locale/uk_UA/LC_MESSAGES/ingo.mo | Bin 0 -> 126496 bytes ingo/locale/zh_CN/LC_MESSAGES/ingo.mo | Bin 0 -> 123711 bytes ingo/locale/zh_TW/LC_MESSAGES/ingo.mo | Bin 0 -> 141657 bytes ingo/po/README | 1 + ingo/po/ca_ES.po | 923 ++++++ ingo/po/cs_CZ.po | 792 ++++++ ingo/po/da_DK.po | 965 +++++++ ingo/po/de_DE.po | 1043 +++++++ ingo/po/el_GR.po | 793 ++++++ ingo/po/es_ES.po | 1044 +++++++ ingo/po/et_EE.po | 1024 +++++++ ingo/po/eu_ES.po | 918 ++++++ ingo/po/fi_FI.po | 1029 +++++++ ingo/po/fr_FR.po | 1054 +++++++ ingo/po/hu_HU.po | 1044 +++++++ ingo/po/ingo.pot | 1019 +++++++ ingo/po/it_IT.po | 1041 +++++++ ingo/po/ja_JP.po | 1038 +++++++ ingo/po/ko_KR.po | 1087 +++++++ ingo/po/lt_LT.po | 1035 +++++++ ingo/po/lv_LV.po | 775 +++++ ingo/po/nb_NO.po | 820 ++++++ ingo/po/nl_NL.po | 1045 +++++++ ingo/po/pl_PL.po | 1043 +++++++ ingo/po/pt_BR.po | 1047 +++++++ ingo/po/pt_PT.po | 932 ++++++ ingo/po/ro_RO.po | 494 ++++ ingo/po/ru_RU.po | 1023 +++++++ ingo/po/sk_SK.po | 1038 +++++++ ingo/po/sl_SI.po | 1016 +++++++ ingo/po/sv_SE.po | 806 ++++++ ingo/po/tr_TR.po | 1037 +++++++ ingo/po/uk_UA.po | 1013 +++++++ ingo/po/zh_CN.po | 998 +++++++ ingo/po/zh_TW.po | 1023 +++++++ ingo/rule.php | 356 +++ ingo/script.php | 70 + ingo/scripts/.htaccess | 1 + ingo/scripts/ingo-postfix-policyd | 173 ++ ingo/scripts/sql/ingo.oci8.sql | 99 + ingo/scripts/sql/ingo.sql | 99 + ingo/scripts/sql/ingo.xml | 556 ++++ ingo/scripts/upgrades/1.2.1_to_1.2.2.sql | 1 + ingo/scripts/upgrades/1.2_to_1.2.1.sql | 2 + .../upgrades/2007-04-25_add_timed_vacation.sql | 2 + .../upgrades/2008-06-17_fix_varchar_lengths.sql | 2 + ingo/scripts/upgrades/2008-09-23_fix_group_uid.sql | 1 + .../upgrades/convert_datatree_shares_to_sql.php | 199 ++ ingo/scripts/upgrades/convert_prefs_to_sql.php | 91 + ingo/spam.php | 167 ++ ingo/templates/blacklist/blacklist.inc | 46 + ingo/templates/common-header.inc | 23 + ingo/templates/filters/filter-none.inc | 5 + ingo/templates/filters/filter.html | 36 + ingo/templates/filters/footer.inc | 11 + ingo/templates/filters/header.inc | 33 + ingo/templates/filters/settings.inc | 35 + ingo/templates/javascript/new_folder.js | 17 + ingo/templates/menu.inc | 20 + ingo/templates/rule/filter.inc | 37 + ingo/templates/rule/footer.inc | 65 + ingo/templates/rule/header.inc | 54 + ingo/templates/script/activate.inc | 39 + ingo/templates/script/footer.inc | 4 + ingo/templates/script/header.inc | 4 + ingo/templates/script/script.inc | 4 + ingo/templates/whitelist/whitelist.inc | 28 + ingo/test.php | 100 + ingo/themes/graphics/blacklist.png | Bin 0 -> 477 bytes ingo/themes/graphics/copy.png | Bin 0 -> 293 bytes ingo/themes/graphics/disable.png | Bin 0 -> 477 bytes ingo/themes/graphics/enable.png | Bin 0 -> 167 bytes ingo/themes/graphics/favicon.ico | Bin 0 -> 1406 bytes ingo/themes/graphics/forward.png | Bin 0 -> 153 bytes ingo/themes/graphics/ingo.png | Bin 0 -> 155 bytes ingo/themes/graphics/script.png | Bin 0 -> 482 bytes ingo/themes/graphics/spam.png | Bin 0 -> 746 bytes ingo/themes/graphics/vacation.png | Bin 0 -> 199 bytes ingo/themes/graphics/whitelist.png | Bin 0 -> 167 bytes ingo/themes/screen.css | 9 + ingo/themes/silver/graphics/blacklist.png | Bin 0 -> 655 bytes ingo/themes/silver/graphics/copy.png | Bin 0 -> 663 bytes ingo/themes/silver/graphics/disable.png | Bin 0 -> 655 bytes ingo/themes/silver/graphics/enable.png | Bin 0 -> 537 bytes ingo/themes/silver/graphics/favicon.ico | Bin 0 -> 1150 bytes ingo/themes/silver/graphics/forward.png | Bin 0 -> 754 bytes ingo/themes/silver/graphics/ingo.png | Bin 0 -> 570 bytes ingo/themes/silver/graphics/script.png | Bin 0 -> 861 bytes ingo/themes/silver/graphics/spam.png | Bin 0 -> 476 bytes ingo/themes/silver/graphics/vacation.png | Bin 0 -> 623 bytes ingo/themes/silver/graphics/whitelist.png | Bin 0 -> 537 bytes ingo/themes/silver/themed_graphics | 0 ingo/themes/tango-blue/graphics/blacklist.png | Bin 0 -> 530 bytes ingo/themes/tango-blue/graphics/copy.png | Bin 0 -> 498 bytes ingo/themes/tango-blue/graphics/disable.png | Bin 0 -> 530 bytes ingo/themes/tango-blue/graphics/enable.png | Bin 0 -> 220 bytes ingo/themes/tango-blue/graphics/favicon.ico | Bin 0 -> 1406 bytes ingo/themes/tango-blue/graphics/forward.png | Bin 0 -> 225 bytes ingo/themes/tango-blue/graphics/ingo.png | Bin 0 -> 213 bytes ingo/themes/tango-blue/graphics/script.png | Bin 0 -> 692 bytes ingo/themes/tango-blue/graphics/spam.png | Bin 0 -> 770 bytes ingo/themes/tango-blue/graphics/vacation.png | Bin 0 -> 239 bytes ingo/themes/tango-blue/graphics/whitelist.png | Bin 0 -> 220 bytes ingo/themes/tango-blue/themed_graphics | 0 ingo/vacation.php | 159 ++ ingo/whitelist.php | 60 + 192 files changed, 48677 insertions(+) create mode 100644 ingo/LICENSE create mode 100644 ingo/README create mode 100644 ingo/blacklist.php create mode 100644 ingo/config/.htaccess create mode 100644 ingo/config/backends.php.dist create mode 100644 ingo/config/conf.xml create mode 100644 ingo/config/fields.php.dist create mode 100644 ingo/config/hooks.php.dist create mode 100644 ingo/config/prefs.php.dist create mode 100644 ingo/docs/CHANGES create mode 100644 ingo/docs/CREDITS create mode 100644 ingo/docs/INSTALL create mode 100644 ingo/docs/RELEASE_NOTES create mode 100644 ingo/docs/TODO create mode 100644 ingo/docs/UPGRADING create mode 100644 ingo/filters.php create mode 100644 ingo/forward.php create mode 100644 ingo/index.php create mode 100644 ingo/lib/Block/overview.php create mode 100644 ingo/lib/Driver.php create mode 100644 ingo/lib/Driver/ldap.php create mode 100644 ingo/lib/Driver/null.php create mode 100644 ingo/lib/Driver/sivtest.php create mode 100644 ingo/lib/Driver/timsieved.php create mode 100644 ingo/lib/Driver/vfs.php create mode 100644 ingo/lib/Ingo.php create mode 100644 ingo/lib/Script.php create mode 100644 ingo/lib/Script/imap.php create mode 100644 ingo/lib/Script/imap/live.php create mode 100644 ingo/lib/Script/imap/mock.php create mode 100644 ingo/lib/Script/maildrop.php create mode 100644 ingo/lib/Script/procmail.php create mode 100644 ingo/lib/Script/sieve.php create mode 100644 ingo/lib/Session.php create mode 100644 ingo/lib/Storage.php create mode 100644 ingo/lib/Storage/mock.php create mode 100644 ingo/lib/Storage/prefs.php create mode 100644 ingo/lib/Storage/sql.php create mode 100644 ingo/lib/Template.php create mode 100644 ingo/lib/UI/VarRenderer/ingo.php create mode 100644 ingo/lib/api.php create mode 100644 ingo/lib/base.php create mode 100644 ingo/lib/tests/MaildropTest.php create mode 100644 ingo/lib/tests/ProcmailTest.php create mode 100644 ingo/lib/tests/ScriptTest.php create mode 100644 ingo/lib/tests/SieveTest.php create mode 100644 ingo/lib/tests/TestBase.php create mode 100644 ingo/lib/tests/_data/from_spammer create mode 100644 ingo/lib/tests/_data/not_from_spammer create mode 100644 ingo/lib/version.php create mode 100644 ingo/locale/ca_ES/LC_MESSAGES/ingo.mo create mode 100755 ingo/locale/ca_ES/help.xml create mode 100644 ingo/locale/cs_CZ/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/da_DK/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/de_DE/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/de_DE/help.xml create mode 100644 ingo/locale/el_GR/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/en_US/help.xml create mode 100644 ingo/locale/es_ES/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/es_ES/help.xml create mode 100644 ingo/locale/et_EE/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/eu_ES/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/eu_ES/help.xml create mode 100644 ingo/locale/fi_FI/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/fi_FI/help.xml create mode 100644 ingo/locale/fr_FR/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/fr_FR/help.xml create mode 100644 ingo/locale/hu_HU/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/it_IT/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/ja_JP/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/ko_KR/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/lt_LT/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/lv_LV/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/lv_LV/help.xml create mode 100644 ingo/locale/nb_NO/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/nl_NL/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/pl_PL/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/pt_BR/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/pt_PT/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/ro_RO/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/ru_RU/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/sk_SK/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/sl_SI/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/sv_SE/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/tr_TR/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/uk_UA/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/zh_CN/LC_MESSAGES/ingo.mo create mode 100644 ingo/locale/zh_TW/LC_MESSAGES/ingo.mo create mode 100644 ingo/po/README create mode 100644 ingo/po/ca_ES.po create mode 100644 ingo/po/cs_CZ.po create mode 100644 ingo/po/da_DK.po create mode 100644 ingo/po/de_DE.po create mode 100644 ingo/po/el_GR.po create mode 100644 ingo/po/es_ES.po create mode 100644 ingo/po/et_EE.po create mode 100644 ingo/po/eu_ES.po create mode 100644 ingo/po/fi_FI.po create mode 100644 ingo/po/fr_FR.po create mode 100644 ingo/po/hu_HU.po create mode 100644 ingo/po/ingo.pot create mode 100644 ingo/po/it_IT.po create mode 100644 ingo/po/ja_JP.po create mode 100644 ingo/po/ko_KR.po create mode 100644 ingo/po/lt_LT.po create mode 100644 ingo/po/lv_LV.po create mode 100644 ingo/po/nb_NO.po create mode 100644 ingo/po/nl_NL.po create mode 100644 ingo/po/pl_PL.po create mode 100644 ingo/po/pt_BR.po create mode 100644 ingo/po/pt_PT.po create mode 100644 ingo/po/ro_RO.po create mode 100644 ingo/po/ru_RU.po create mode 100644 ingo/po/sk_SK.po create mode 100644 ingo/po/sl_SI.po create mode 100644 ingo/po/sv_SE.po create mode 100644 ingo/po/tr_TR.po create mode 100644 ingo/po/uk_UA.po create mode 100644 ingo/po/zh_CN.po create mode 100644 ingo/po/zh_TW.po create mode 100644 ingo/rule.php create mode 100644 ingo/script.php create mode 100644 ingo/scripts/.htaccess create mode 100755 ingo/scripts/ingo-postfix-policyd create mode 100644 ingo/scripts/sql/ingo.oci8.sql create mode 100644 ingo/scripts/sql/ingo.sql create mode 100644 ingo/scripts/sql/ingo.xml create mode 100644 ingo/scripts/upgrades/1.2.1_to_1.2.2.sql create mode 100644 ingo/scripts/upgrades/1.2_to_1.2.1.sql create mode 100644 ingo/scripts/upgrades/2007-04-25_add_timed_vacation.sql create mode 100644 ingo/scripts/upgrades/2008-06-17_fix_varchar_lengths.sql create mode 100644 ingo/scripts/upgrades/2008-09-23_fix_group_uid.sql create mode 100755 ingo/scripts/upgrades/convert_datatree_shares_to_sql.php create mode 100755 ingo/scripts/upgrades/convert_prefs_to_sql.php create mode 100644 ingo/spam.php create mode 100644 ingo/templates/blacklist/blacklist.inc create mode 100644 ingo/templates/common-header.inc create mode 100644 ingo/templates/filters/filter-none.inc create mode 100644 ingo/templates/filters/filter.html create mode 100644 ingo/templates/filters/footer.inc create mode 100644 ingo/templates/filters/header.inc create mode 100644 ingo/templates/filters/settings.inc create mode 100644 ingo/templates/javascript/new_folder.js create mode 100644 ingo/templates/menu.inc create mode 100644 ingo/templates/rule/filter.inc create mode 100644 ingo/templates/rule/footer.inc create mode 100644 ingo/templates/rule/header.inc create mode 100644 ingo/templates/script/activate.inc create mode 100644 ingo/templates/script/footer.inc create mode 100644 ingo/templates/script/header.inc create mode 100644 ingo/templates/script/script.inc create mode 100644 ingo/templates/whitelist/whitelist.inc create mode 100644 ingo/test.php create mode 100644 ingo/themes/graphics/blacklist.png create mode 100644 ingo/themes/graphics/copy.png create mode 100644 ingo/themes/graphics/disable.png create mode 100644 ingo/themes/graphics/enable.png create mode 100644 ingo/themes/graphics/favicon.ico create mode 100644 ingo/themes/graphics/forward.png create mode 100644 ingo/themes/graphics/ingo.png create mode 100644 ingo/themes/graphics/script.png create mode 100644 ingo/themes/graphics/spam.png create mode 100644 ingo/themes/graphics/vacation.png create mode 100644 ingo/themes/graphics/whitelist.png create mode 100644 ingo/themes/screen.css create mode 100644 ingo/themes/silver/graphics/blacklist.png create mode 100644 ingo/themes/silver/graphics/copy.png create mode 100644 ingo/themes/silver/graphics/disable.png create mode 100644 ingo/themes/silver/graphics/enable.png create mode 100644 ingo/themes/silver/graphics/favicon.ico create mode 100644 ingo/themes/silver/graphics/forward.png create mode 100644 ingo/themes/silver/graphics/ingo.png create mode 100644 ingo/themes/silver/graphics/script.png create mode 100644 ingo/themes/silver/graphics/spam.png create mode 100644 ingo/themes/silver/graphics/vacation.png create mode 100644 ingo/themes/silver/graphics/whitelist.png create mode 100644 ingo/themes/silver/themed_graphics create mode 100644 ingo/themes/tango-blue/graphics/blacklist.png create mode 100644 ingo/themes/tango-blue/graphics/copy.png create mode 100644 ingo/themes/tango-blue/graphics/disable.png create mode 100644 ingo/themes/tango-blue/graphics/enable.png create mode 100644 ingo/themes/tango-blue/graphics/favicon.ico create mode 100644 ingo/themes/tango-blue/graphics/forward.png create mode 100644 ingo/themes/tango-blue/graphics/ingo.png create mode 100644 ingo/themes/tango-blue/graphics/script.png create mode 100644 ingo/themes/tango-blue/graphics/spam.png create mode 100644 ingo/themes/tango-blue/graphics/vacation.png create mode 100644 ingo/themes/tango-blue/graphics/whitelist.png create mode 100644 ingo/themes/tango-blue/themed_graphics create mode 100644 ingo/vacation.php create mode 100644 ingo/whitelist.php diff --git a/.gitignore b/.gitignore index cf2cfe42f..79e764ee2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,6 @@ iPhoto2Ansel/iPhoto2Ansel.xcodeproj/*.pbxuser iPhoto2Ansel/iPhoto2Ansel.xcodeproj/*.perspectivev3 iPhoto2Ansel/*.pbxproject imp/config/*.php +ingo/config/*.php jeta/config/*.php kronolith/config/*.php diff --git a/ingo/LICENSE b/ingo/LICENSE new file mode 100644 index 000000000..198b72e52 --- /dev/null +++ b/ingo/LICENSE @@ -0,0 +1,49 @@ +Version 1.0 + +Copyright 2002-2009 The Horde Project (http://www.horde.org/) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. The end-user documentation included with the redistribution, if +any, must include the following acknowledgment: + + "This product includes software developed by the Horde Project + (http://www.horde.org/)." + +Alternately, this acknowledgment may appear in the software itself, if +and wherever such third-party acknowledgments normally appear. + +4. The names "Horde", "The Horde Project", and "Ingo" must not be used +to endorse or promote products derived from this software without +prior written permission. For written permission, please contact +core@horde.org. + +5. Products derived from this software may not be called "Horde" or +"Ingo", nor may "Horde" or "Ingo" appear in their name, without prior +written permission of the Horde Project. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE HORDE PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This software consists of voluntary contributions made by many +individuals on behalf of the Horde Project. For more information on +the Horde Project, please see . diff --git a/ingo/README b/ingo/README new file mode 100644 index 000000000..cc1ef3552 --- /dev/null +++ b/ingo/README @@ -0,0 +1,85 @@ +What is Ingo? +============== + +:Contact: ingo@lists.horde.org + +.. contents:: Contents +.. section-numbering:: + +Ingo is an email filter rules manager. It can generate Sieve and procmail +scripts and upload them to the server (using a timsieved or VFS FTP driver, +respectively). It can also create and execute IMAP commands to run filter +rules. + +This software is OSI Certified Open Source Software. OSI Certified is a +certification mark of the `Open Source Initiative`_. + +.. _`Open Source Initiative`: http://www.opensource.org/ + + +Obtaining Ingo +-------------- + +Further information on Ingo and the latest version can be obtained at + + http://www.horde.org/ingo/ + + +Documentation +------------- + +The following documentation is available in the Ingo distribution: + +:README_: This file +:LICENSE_: Copyright and license information +:`docs/CHANGES`_: Changes by release +:`docs/CREDITS`_: Project developers +:`docs/INSTALL`_: Installation instructions and notes +:`docs/TODO`_: Development TODO list + + +Installation +------------ + +Instructions for installing Ingo can be found in the file INSTALL_ in the +``docs/`` directory of the Ingo distribution. + + +Assistance +---------- + +If you encounter problems with Ingo, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users also make occasional +appearances on IRC, on the channel #horde on the Freenode Network +(irc.freenode.net). + + +Licensing +--------- + +For licensing and copyright information, please see the file LICENSE_ +in the Ingo distribution. + +Thanks, + +The Ingo team + + +.. _README: ?f=README.html +.. _LICENSE: http://www.horde.org/licenses/asl.php +.. _docs/CHANGES: ?f=CHANGES.html +.. _docs/CREDITS: ?f=CREDITS.html +.. _INSTALL: +.. _docs/INSTALL: ?f=INSTALL.html +.. _docs/TODO: ?f=TODO.html diff --git a/ingo/blacklist.php b/ingo/blacklist.php new file mode 100644 index 000000000..72c3c94b1 --- /dev/null +++ b/ingo/blacklist.php @@ -0,0 +1,108 @@ + + * @author Michael Slusarz + */ + +require_once dirname(__FILE__) . '/lib/base.php'; + +/* Redirect if blacklist is not available. */ +if (!in_array(Ingo_Storage::ACTION_BLACKLIST, $_SESSION['ingo']['script_categories'])) { + $notification->push(_("Blacklist is not supported in the current filtering driver."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; +} + +/* Get the backend. */ +$scriptor = Ingo::loadIngoScript(); + +/* Determine if this scriptor supports mark-as-deleted. */ +$have_mark = $scriptor && in_array(Ingo_Storage::ACTION_FLAGONLY, $scriptor->availableActions()); + +/* Get the blacklist object. */ +$blacklist = &$ingo_storage->retrieve(Ingo_Storage::ACTION_BLACKLIST); +if (is_a($blacklist, 'PEAR_Error')) { + $notification->push($blacklist); + $blacklist = new Ingo_Storage_Blacklist(); +} +$folder = $blacklist_folder = null; + +/* Perform requested actions. */ +$actionID = Util::getFormData('actionID'); +switch ($actionID) { +case 'create_folder': + $blacklist_folder = Ingo::createFolder(Util::getFormData('new_folder_name')); + break; + +case 'rule_update': + switch (Util::getFormData('action')) { + case 'delete': + $folder = ''; + break; + + case 'mark': + $folder = Ingo::BLACKLIST_MARKER; + break; + + case 'folder': + $folder = Util::getFormData('actionvalue'); + break; + } + + if (($folder == Ingo::BLACKLIST_MARKER) && !$have_mark) { + $notification->push("Not supported by this script generator.", 'horde.error'); + } else { + $ret = $blacklist->setBlacklist(Util::getFormData('blacklist')); + if (is_a($ret, 'PEAR_Error')) { + $notification->push($ret, $ret->getCode()); + } else { + $blacklist->setBlacklistFolder($folder); + if (!$ingo_storage->store($blacklist)) { + $notification->push(_("Error saving changes."), 'horde.error'); + } else { + $notification->push(_("Changes saved."), 'horde.success'); + } + + if ($prefs->getValue('auto_update')) { + /* This does its own $notification->push() on error: */ + Ingo::updateScript(); + } + } + + /* Update the timestamp for the rules. */ + $_SESSION['ingo']['change'] = time(); + } + + break; +} + +/* Create the folder listing. */ +if (!isset($blacklist_folder)) { + $blacklist_folder = $blacklist->getBlacklistFolder(); +} +$field_num = $have_mark ? 2 : 1; +$folder_list = Ingo::flistSelect($blacklist_folder, 'filters', 'actionvalue', + 'document.filters.action[' . $field_num . + '].checked=true'); + +/* Get the blacklist rule. */ +$filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); +$bl_rule = $filters->findRule(Ingo_Storage::ACTION_BLACKLIST); + +/* Include new folder JS if necessary. */ +if ($registry->hasMethod('mail/createFolder')) { + Horde::addScriptFile('new_folder.js'); +} + +$title = _("Blacklist Edit"); +require INGO_TEMPLATES . '/common-header.inc'; +require INGO_TEMPLATES . '/menu.inc'; +require INGO_TEMPLATES . '/blacklist/blacklist.inc'; +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/ingo/config/.htaccess b/ingo/config/.htaccess new file mode 100644 index 000000000..3a4288278 --- /dev/null +++ b/ingo/config/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/ingo/config/backends.php.dist b/ingo/config/backends.php.dist new file mode 100644 index 000000000..c86142609 --- /dev/null +++ b/ingo/config/backends.php.dist @@ -0,0 +1,327 @@ + 'null', + 'preferred' => 'example.com', + 'hordeauth' => true, + 'params' => array(), + 'script' => 'imap', + 'scriptparams' => array(), + 'shares' => false +); + +/* Maildrop Example */ +$backends['maildrop'] = array( + 'driver' => 'vfs', + 'preferred' => 'example.com', + 'hordeauth' => true, + 'params' => array( + // Hostname of the VFS server + 'hostspec' => 'ftp.example.com', + // Name of the maildrop config file to write + 'filename' => '.mailfilter', + // The VFS username to use, defaults to current user. If you want to + // use a different user, you also need to disable 'hordeauth' above. + // 'username' => 'user', + // The VFS password to use, defaults to current user's password + // 'password' => 'secret', + // The path to the .mailfilter filter file, defaults to the filters' + // owner's home directory. + // You can use the following variables: + // %u = name of the filters' owner + // %d = domain name of the filters' owner + // %U = the 'username' from above + // Example: + // '/data/maildrop/filters/%d/%u' + // This would be translated into: + // '/data/maildrop/filters///.mailfilter' + // 'vfs_path' => '/path/to/maildrop', + + // VFS: FTP example + // The VFS driver to use + 'vfstype' => 'ftp', + // Port of the VFS server + 'port' => 21, + // Specify permissions for uploaded files if necessary: + // 'file_perms' => '0640', + + // VFS: SSH2 example + // The VFS driver to use + // 'vfstype' => 'ssh2', + // Port of the VFS server + // 'port' => 22, + ), + 'script' => 'maildrop', + 'scriptparams' => array( + // What path style does the IMAP server use ['mbox'|'maildir']? + 'path_style' => 'mbox', + // Strip 'INBOX.' from the beginning of folder names in generated + // scripts? + 'strip_inbox' => false, + // An array of variables to append to every generated script. + // Use if you need to set up specific environment variables. + 'variables' => array( + // Example for the $PATH variable + // 'PATH' => '/usr/bin' + ) + ), + 'shares' => false +); + +/* Procmail Example */ +$backends['procmail'] = array( + 'driver' => 'vfs', + 'preferred' => 'example.com', + 'hordeauth' => true, + 'params' => array( + // Hostname of the VFS server + 'hostspec' => 'ftp.example.com', + // Name of the procmail config file to write + 'filename' => '.procmailrc', + // The VFS username to use, defaults to current user. If you want to + // use a different user, you also need to disable 'hordeauth' above. + // 'username' => 'user', + // The VFS password to use, defaults to current user's password + // 'password' => 'secret', + // The path to the .procmailrc filter file, defaults to the filters' + // owner's home directory. + // You can use the following variables: + // %u = name of the filters' owner + // %U = the 'username' from above + // Example: + // '/data/procmail/filters/%u' + // This would be translated into: + // '/data/procmail/filters//.procmailrc' + // 'vfs_path' => '/path/to/procmail', + + // If procmail needs an external command for mail delivery, you + // can specify it below. You can also set a prefix for the mailbox name + // eg. for /usr/local/sbin/dmail +INBOX + // 'delivery_agent' => '/usr/local/sbin/dmail', + // 'delivery_mailbox_prefix' => '+', + + // If you need procmail to be called from .forward in the user's home + // directory, set the file and the content below: + // 'forward_file' => '.forward', + // 'forward_string' => '"|/usr/local/bin/procmail"', + + // if the GNU utilities cannot be found in the path + // or have different names, you can specify their location below + // 'date' => '/opt/csw/bin/gdate', + // 'echo' => '/opt/csw/bin/gecho', + // 'ls' => '/opt/csw/bin/gls', + + // VFS: FTP example + // The VFS driver to use + 'vfstype' => 'ftp', + // Port of the VFS server + 'port' => 21, + + // VFS: SSH2 example + // The VFS driver to use + // 'vfstype' => 'ssh2', + // Port of the VFS server + // 'port' => 22, + ), + 'script' => 'procmail', + 'scriptparams' => array( + // What path style does the IMAP server use ['mbox'|'maildir']? + 'path_style' => 'mbox', + // An array of variables to append to every generated script. + // Use if you need to set up specific environment variables. + 'variables' => array( + // Example for the $PATH variable + // 'PATH' => '/usr/bin', + // Example for the $DEFAULT variable + // 'DEFAULT' => '$HOME/Maildir', + // Example for the $VACATION_DIR variable (used to store vacation files) + // 'VACATION_DIR' => '$HOME', + ) + ), + 'shares' => false +); + +/* Sieve Example */ +$backends['sieve'] = array( + 'driver' => 'timsieved', + 'preferred' => 'example.com', + 'hordeauth' => true, + 'params' => array( + // Hostname of the timsieved server + 'hostspec' => 'mail.example.com', + // Login type of the server + 'logintype' => 'PLAIN', + // Enable/disable TLS encryption + 'usetls' => true, + // Port number of the timsieved server + 'port' => 2000, + // Name of the sieve script + 'scriptname' => 'ingo', + // The following settings can be used to specify an administration + // user to update all users' scripts. If you want to use an admin + // user, you also need to disable 'hordeauth' above. You have to use + // an admin user if you want to use shared rules. + // 'username' => 'cyrus', + // 'password' => '*****', + ), + 'script' => 'sieve', + 'scriptparams' => array(), + 'shares' => false +); + +/* sivtest Example */ +$backends['sivtest'] = array( + 'driver' => 'sivtest', + 'preferred' => 'example.com', + 'hordeauth' => true, + 'params' => array( + // Hostname of the timsieved server + 'hostspec' => 'mail.example.com', + // Login type of the server + 'logintype' => 'GSSAPI', + // Enable/disable TLS encryption + 'usetls' => true, + // Port number of the timsieved server + 'port' => 2000, + // Name of the sieve script + 'scriptname' => 'ingo', + // Location of sivtest + 'command' => '/usr/bin/sivtest', + // name of the socket we're using + 'socket' => Horde::getTempDir() . '/sivtest.' + . md5(uniqid(rand())) . '.sock', + ), + 'script' => 'sieve', + 'scriptparams' => array(), + 'shares' => false, +); + +/* Sun ONE/JES Example (LDAP/Sieve) */ +$backends['ldapsieve'] = array( + 'driver' => 'ldap', + 'preferred' => 'example.com', + 'hordeauth' => false, + 'params' => array( + // + // Hostname of the ldap server + // + 'hostspec' => 'ldap.example.com', + // + // Port number of the timsieved server + // + 'port' => 389, + // + // LDAP Protocol Version (default = 2). 3 is required for TLS. + // + 'version' => 3, + // + // Whether or not to use TLS. If using TLS, you MUST configure + // OpenLDAP (either /etc/ldap.conf or /etc/ldap/ldap.conf) with the CA + // certificate which signed the certificate of the server to which you + // are connecting. e.g.: + // + // TLS_CACERT /usr/share/ca-certificates/mozilla/Equifax_Secure_CA.crt + // + // You MAY have problems if you are using TLS and your server is + // configured to make random referrals, since some OpenLDAP libraries + // appear to check the certificate against the original domain name, + // and not the referred-to domain. This can be worked around by + // putting the following directive in the ldap.conf: + // + // TLS_REQCERT never + // + 'tls' => true, + // + // Bind DN (for bind and script distinguished names, %u is replaced + // with username, and %d is replaced with the internet domain + // components (e.g. "dc=example, dc=com") if available). + // + 'bind_dn' => 'cn=ingo, ou=applications, dc=example, dc=com', + // + // Bind password. If not provided, user's password is used (useful + // when bind_dn contains %u). + // + 'bind_password' => 'secret', + // + // How to find user object. + // + 'script_base' => 'ou=People, dc=example, dc=com', + 'script_filter' => '(uid=%u)', + // + // Attribute script is stored in. Will not touch non-Ingo scripts. + // + 'script_attribute' => 'mailSieveRuleSource' + ), + 'script' => 'sieve', + 'scriptparams' => array() +); + +/* Kolab Example (using Sieve) */ +if ($GLOBALS['conf']['kolab']['enabled']) { + require_once 'Horde/Kolab.php'; + + if (!is_callable('Kolab', 'getServer')) { + $server = $GLOBALS['conf']['kolab']['imap']['server']; + } else { + $server = Kolab::getServer('imap'); + } + + $backends['kolab'] = array( + 'driver' => 'timsieved', + 'preferred' => '', + 'hordeauth' => 'full', + 'params' => array( + 'hostspec' => $server, + 'logintype' => 'PLAIN', + 'usetls' => false, + 'port' => $GLOBALS['conf']['kolab']['imap']['sieveport'], + 'scriptname' => 'kmail-vacation.siv' + ), + 'script' => 'sieve', + 'scriptparams' => array(), + 'shares' => false + ); +} diff --git a/ingo/config/conf.xml b/ingo/config/conf.xml new file mode 100644 index 000000000..d191865ea --- /dev/null +++ b/ingo/config/conf.xml @@ -0,0 +1,70 @@ + + + + + Menu settings + + + + + + + + + Ingo Rules Storage + prefs + + + + + + + + 0 + 0 + + + + Ingo Rules Options + true + true + + + + Spam Filtering + string + + X-Spam-Score + + + X-Spam-Level + * + + + + + + Custom Hooks + false + true + + diff --git a/ingo/config/fields.php.dist b/ingo/config/fields.php.dist new file mode 100644 index 000000000..2c77ff68d --- /dev/null +++ b/ingo/config/fields.php.dist @@ -0,0 +1,108 @@ + array( + * MANDATORY: + * 'label' => (string) The gettext label for the entry. + * 'type' => (integer) The type of test. Either: + * Ingo_Storage::TYPE_HEADER -- Header test + * Ingo_Storage::TYPE_SIZE -- Message size test + * Ingo_Storage::TYPE_BODY -- Body test + * OPTIONAL: + * 'tests' => (array) Force these tests to be used only. + * If not set, will use the fields generally + * available to the driver. + * ) + * + * $Id$ + */ +$ingo_fields = array( + 'To' => array( + 'label' => _("To"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Subject' => array( + 'label' => _("Subject"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Sender' => array( + 'label' => _("Sender"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'From' => array( + 'label' => _("From"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Cc' => array( + 'label' => _("Cc"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Bcc' => array( + 'label' => _("Bcc"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Resent-from' => array( + 'label' => _("Resent-From"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Resent-to' => array( + 'label' => _("Resent-To"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'List-Id' => array( + 'label' => _("List-ID"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Received' => array( + 'label' => _("Received"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'X-Spam-Level' => array( + 'label' => _("X-Spam-Level"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'X-Spam-Score' => array( + 'label' => _("X-Spam-Score"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'X-Spam-Status' => array( + 'label' => _("X-Spam-Status"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'X-Priority' => array( + 'label' => _("X-Priority"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'To,Cc,Bcc,Resent-to' => array( + 'label' => _("Destination (To, Cc, Bcc, etc.)"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'From,Sender,Reply-to,Resent-from' => array( + 'label' => _("Source (From, Reply-to, etc.)"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'To,Cc,Bcc,Resent-to,From,Sender,Reply-to,Resent-from' => array( + 'label' => _("Participant (From, To, etc.)"), + 'type' => Ingo_Storage::TYPE_HEADER + ), + 'Size' => array( + 'label' => _("Size"), + 'type' => Ingo_Storage::TYPE_SIZE, + 'tests' => array('greater than', 'less than') + ), + 'Body' => array( + 'label' => _("Body"), + 'type' => Ingo_Storage::TYPE_BODY, + 'tests' => array('contains', 'not contain', 'is', 'not is', 'begins with', + 'not begins with', 'ends with', 'not ends with', 'regex', + 'matches', 'not matches') + ) +); diff --git a/ingo/config/hooks.php.dist b/ingo/config/hooks.php.dist new file mode 100644 index 000000000..83ebf9b0a --- /dev/null +++ b/ingo/config/hooks.php.dist @@ -0,0 +1,24 @@ + _("Other Options"), + 'label' => _("Script Updating"), + 'desc' => _("Options about script updating."), + 'members' => array('auto_update')); +} + +// The following preferences are only used for Horde_Script:: drivers that use +// scripts. + +// Automatically update the script? +$_prefs['auto_update'] = array( + 'value' => 1, + 'locked' => false, + 'shared' => false, + 'type' => 'checkbox', + 'desc' => _("Automatically update the script after each change?") +); + +// End script preferences + +// The following preferences are only used for Horde_Script:: drivers that can +// do on-demand filtering. + +// Show detailed filter status messages? +// a value of 0 = no, 1 = yes +$_prefs['show_filter_msg'] = array( + 'value' => 1, + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Only filter [un]seen messages? +// Values: 0, Ingo_Script::FILTER_UNSEEN, Ingo_Script::FILTER_SEEN +$_prefs['filter_seen'] = array( + 'value' => 0, + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// End on-demand filtering preferences + +// If NOT using the 'prefs' storage driver (see conf.php), you can comment out +// the below entries. + +// Filter rules. +$_prefs['rules'] = array( + 'value' => 'a:5:{i:0;a:2:{s:4:"name";s:9:"Whitelist";s:6:"action";i:' . Ingo_Storage::ACTION_WHITELIST . ';}i:1;a:3:{s:4:"name";s:8:"Vacation";s:6:"action";i:' . Ingo_Storage::ACTION_VACATION . ';s:7:"disable";b:1;}i:2;a:2:{s:4:"name";s:9:"Blacklist";s:6:"action";i:' . Ingo_Storage::ACTION_BLACKLIST . ';}i:3;a:3:{s:4:"name";s:11:"Spam Filter";s:6:"action";i:' . Ingo_Storage::ACTION_SPAM . ';s:7:"disable";b:1;}i:4;a:2:{s:4:"name";s:7:"Forward";s:6:"action";i:' . Ingo_Storage::ACTION_FORWARD . ';}}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Blacklist. +// Lock this preference to disable blacklists. +$_prefs['blacklist'] = array( + 'value' => 'a:2:{s:1:"a";a:0:{}s:1:"f";s:0:"";}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Whitelist. +// Lock this preference to disable whitelists. +$_prefs['whitelist'] = array( + 'value' => 'a:0:{}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Vacation notices. +// Lock this preference to disable vacation notices. +$_prefs['vacation'] = array( + 'value' => 'a:8:{s:9:"addresses";a:0:{}s:4:"days";i:7;s:8:"excludes";a:0:{}s:10:"ignorelist";b:1;s:6:"reason";s:0:"";s:7:"subject";s:0:"";s:5:"start";i:0;s:3:"end";i:0;}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Forwarding. +// Lock this preference to disable forwarding. +$_prefs['forward'] = array( + 'value' => 'a:2:{s:1:"a";a:0:{}s:1:"k";i:0;}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// Spam rule. +// Lock this preference to disable the spam rule. +$_prefs['spam'] = array( + 'value' => 'a:2:{s:6:"folder";N;s:5:"level";i:5;}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); diff --git a/ingo/docs/CHANGES b/ingo/docs/CHANGES new file mode 100644 index 000000000..c4164459e --- /dev/null +++ b/ingo/docs/CHANGES @@ -0,0 +1,394 @@ +-------- +v2.0-git +-------- + + + + +---------- +v1.2.2-cvs +---------- + +[cjh] Fix checks for the forward_file and forward_script procmail parameters + in the VFS driver (Bug #7811). +[cjh] Add %d (domain) as a replaceable variable in the vfs_path parameter + (dev@stean.ch, Request #7503). +[jan] Make it easier to select the "Create new folder" entry for target folders + (Bug #7768). +[cjh] Fix reading the current script in the VFS driver (rsalmon@mbpgroup.com, + Bug #7610). +[cjh] Maildrop vacation rule: don't reply to bulk/list messages, add start/end + times, better character support in the message, allow subsequent rules + to execute (rsalmon@mbpgroup.com, Bug #7140). +[cjh] Fix compound header tests in the procmail and maildrop drivers + (rsalmon@mbpgroup.com, Bugs #7117, #7611). +[jan] Change group field in shares table to work with LDAP groups (Bug #6883). + + +------ +v1.2.1 +------ + +[jan] Don't show script icon in menu if preference to automatically update + scripts is both set and locked (Request #7251). +[jan] Fix adding more than 5 wildcard blacklists (Bug #7077). +[jan] Add Basque translation (Euskal Herriko Unibertsitatea EHU/UPV + ). +[jan] Fix saving spam rules in the preference backend (Bug #7033). +[jan] Correctly catch dates before the 10th of a month in Sieve timed + vacations (Bug #7023). +[mas] Fix vacation with procmail driver (Bugs #6509, #7052). +[jan] Fix moving to INBOX with the maildrop driver. +[jan] Disable drop down entries with informational purpose only. +[jan] Fix filter activity messages with non-ASCII folder names + (taguchi@iij.ad.jp, Bug #6764). + + +---- +v1.2 +---- + +[cjh] Improve resource usage in datatree_to_sql share migration script + (Bug #6740). + + +-------- +v1.2-RC3 +-------- + +[cjh] Apply fix for http://dev.rubyonrails.org/ticket/11473 to prototype.js + (Request #6590). +[cjh] Procmail driver: make the directory in which vacation files are + generated configurable (jas@cse.yorku.ca, Request #6643). +[cjh] Add an upgrade script for the new SQL share driver (Request #6109). +[cjh] Procmail driver: include the original email subject in vacation + replies (Michael.Redinger@uibk.ac.at, Request #6432). +[cjh] Make the procmail delivery agent configurable, allow configuring a + prefix for mailboxes (+ is needed for dmail, for example), don't + generate empty VFS files, write a .forward file if needed to pass + mail to procmail (Michael.Redinger@uibk.ac.at, Request #6433). +[jan] Add Slovak translation (Martin Matuška ). +[cjh] Make the command line utilities called from procmail configurable + (Michael.Redinger@uibk.ac.at, Request #6431). +[jan] Fix creating of duplicate special rules when converting from preferences + to SQL storage backend (Bug #6182). +[jan] Follow Sieve specifications more closely when disabling Sieve scripts + (Bug #6337). +[jan] Load default set of filters for SQL storage from config/prefs.php + (tinu@humbapa.ch, Request #6096). +[jan] Add Russian translation (Alexey Zakharov ). + + +-------- +v1.2-RC2 +-------- + +[cjh] Add API methods for setting and disabling vacation rules + (duck@obala.net). +[mas] Fix procmail metacharacter quoting. (Bug #5581) +[cjh] Fix procmail vacation recipe (Volker Then ). +[cjh] Fix position of closing quote in maildrop driver (laxis@magex.hu, + Bug #6020). +[jan] Fix paths in the script for converting to SQL storage (Bug #5957). + + +-------- +v1.2-RC1 +-------- + +[cjh] Allow specifying permissions to set in the VFS for uploaded scripts + (steinkel@ctinetworks.com, Request #5871). +[cjh] Add spam filter actions and numeric comparisons to the Maildrop + script driver (Request #5843). +[cjh] Maildrop driver improvements: make INBOX-stripping configurable; + fix REJECT action; and add is, not is, not contain, not begins + with, not ends with, matches, not matches, exists, and not + exists rules (horde@nospam.obeliks.de, Request #5816). +[jan] Add Japanese translation (Hiromi Kimura ). +[jan] Improve forward rule generation with sieve scripts (Request #5746). +[jan] Move all Ingo-specific hook examples from Horde's config/ directory. +[cjh] Only expunge messages that we have moved or deleted completely when + using IMAP filters (Bug #4749). +[cjh] Fix behavior of IMAP stop-script rules (Bug #5539). +[cjh] Shouldn't show an input field for exists or not exists tests (Bug #5659). +[cjh] Don't let users save rules with empty conditions (Bug #5641). +[cjh] Match email addresses exactly for procmail vacation rules, and include + the Cc: and Bcc: fields in checks for which address the message was + sent to (Bug #4333). +[cjh] Ingo now provides a Postfix policy daemon that can be used to enforce + blacklist and whitelist rules at delivery time (Request #4904). +[cjh] Autocreate VFS paths when saving rules (stevekwok@hotmail.com>). + + +---------- +v1.2-ALPHA +---------- + +[cjh] Add reverse conditions for procmail (horde@coursimault.com, Request + #4037). +[cjh] Allow numeric spam score comparisons with Sieve (adrieder@sbox.tugraz.at, + Request #3837). +[jan] Add Ukrainian translation (Andriy Kopystyansky ). +[cjh] Add timed vacation messages (groente@puscii.nl, Request #4938). +[mas] Add body test for sieve (michael.menge@zdv.uni-tuebingen.de, Request + #3875). +[mas] Change 'Important' flag to 'Flagged For Followup' to match IMP. +[jan] Add full character set support to vacation messages in maildrop and + procmail drivers (Requests #4034, #4989). +[cjh] Add sivtest driver (liamr@deathstar.org, Request #4777). +[cjh] Add maildrop vacation support (jrvs@bothends.org, Request #3722). +[jan] Add hook to provide addresses for vacation messages (Request #4542). +[jan] Add "Save and Enable/Disable" buttons to forward, vacation, and spam + screens (Request #4543). +[jan] Group vacation settings in tabs (Request #4541). +[mas] Conform to WCAG 1.0 Priority 2/Section 508 accessibility guidelines. + (Request #4080) +[mas] Check sieve quota before uploading a script. (gergely@risko.hu, Request + #4278) +[jmf] Blacklist and whitelist for imap scripts now work consistently with + sieve scripts -- must match full address. +[ben] Add body test for procmail (horde@coursimault.com, + Requests #3874, #4035). +[cjh] Add ssh2 to the supported VFS backends (Cliff Green ). +[cjh] Add checks to avoid mail loops in the procmail forwards code + (Request #3502). +[jan] Use Horde_Form API to generate special rule forms. +[jan] Add multidomain support for Kolab servers (tokoe@kde.org, Request #3579). +[jan] Add support for changing other users' filter rules. +[jan] Add SQL storage driver. +[jmf] Add simple spam-filtering setup page. + + +------ +v1.1.5 +------ + +[jan] Add Japanese translation (Hiromi Kimura ). +[jan] Improve forward rule generation with sieve scripts (Request #5746). + + +------ +v1.1.4 +------ + +[jan] Add Simplified Chinese translation (Anna Chen + ). +[cjh] Fix check for bulk or list precedence in Sieve vacation (Bug #5543). +[jan] Add additional checks to avoid adding empty test values (Bug #5392). +[jan] Fix multiline vacation messages with procmail driver (Bug #5273). + + +------ +v1.1.3 +------ + +[jan] Fix blacklists and whitelists in the IMAP driver. + + +------ +v1.1.2 +------ + +[ben] SECURITY: Make sure folder names are properly escaped in procmail driver + to prevent local code execution (Bug #4513). +[jan] Match all messages with Sieve driver if not providing any criteria + (gergely@risko.hu, Bug #4345). +[jan] Add parameter to enable or disable TLS in timsieved driver (Marcus Hüwe + ). +[jan] Add Slovenian translation (Duck ). + + +------ +v1.1.1 +------ + +[jan] Enable filter setting to stop further filtering by default. +[jan] Correctly escape forward slashes in maildrop driver (Bug #3514). +[jan] Add additional checks to avoid adding empty test values (Bug #3313). +[jan] Add Estonian translation (Toomas Aas ). +[jan] Add Greek translation (Konstantinos C. Milosis ). +[mms] Fix backward compatibility break with Ingo 1.1 and Horde < 3.1. +[cjh] Add missing stripe.js (Bug #3623). + + +---- +v1.1 +---- + +[jan] Automatically add missing special rules when editing them (Bug #3325). + + +-------- +v1.1-RC1 +-------- + +[jan] Add portal block for special rules (Oliver Kuhl , + Request #3106). +[jan] Add Portuguese translation (Manuel Menezes de Sequeira + ). +[cjh] Fix escaping of output in several cases. +[mas] Add notify rule. (Paul Wolstenholme , Bug #1139) +[jan] Add Turkish translation (METU ). +[jan] Don't activate script if exceeded blacklist/whitelist limits (Bug #1166). +[jan] Add Danish translation (Brian Truelsen ). +[jan] Allow to use a single user for VFS drivers (maddog2k@maddog2k.net, + Request #2324). +[jan] Allow to move rules by specifying their positions (Gergely Risko + , Request #1060). +[mas] Add flag-only rule. (Request #1114) +[ben] Hide procmail vacation db files +[mms] Add maildrop script driver (Matt Weyland ). +[ben] Add support for '^TO_' procmail rule matching. +[ben] Add support for special headers on a per-driver basis. +[jmf] Add support for storing Sieve rules in LDAP (Supports Sun JES/ONE and + iPlanet messaging servers). +[jan] Add permissions to restrict filter rule creation. + + +------ +v1.0.2 +------ + +[jan] Add Korean translation (Jinhyok Heo ). + + +---------- +v1.0.2-RC1 +---------- + +[jan] Convert rules from Ingo 1.0 with multibyte characters (Bug #1282). +[jan] Always encode Sieve scripts in UTF-8 and MIME encode subject of vacation + messages (Gergely Risko , Bug #2121). +[cjh] Don't escape "\" in Sieve regex mode (Bug #2134). +[jan] Don't allow empty addresses in blacklists and whitelists (Bug #2165). +[jan] Really show the active script if clicking that button (Bug #2135). +[jan] Fix migration script for IMP filters to not include folder rules from + already migrated users (Bug #2114). +[jan] Fix creating of non-ascii IMAP folder names (Bug #2054). +[jan] Fix regular expression rules that contain commas (Bug #1904). +[mas] Allow the disabling of blacklist, whitelist, vacation, and forward + (Request #1164). +[jan] Allow message flags with all sieve rules that keep the message + (Bug #1292). +[jan] Add Norwegian Bokmaal translation (Trond Bjørstad , Thomas + Chr. Dahl ). +[jan] Change whitelist rule for Sieve to not apply any further rules + (Bug #1378). +[jan] Add shortcut icon (favicon.ico). +[ben] Fix procmail forwarding. (hager (at) fh-rosenheim (dot) de, Bug #1125) +[jan] Allow to specify multiple values with "Begins/Ends With" tests (Todd + Merritt , Bug #1105). + + +------ +v1.0.1 +------ + +[jan] Fix blacklist conversion in IMP-to-Ingo conversion script (Steve Lidie + ). +[jan] Fix filter rules with multibyte characters. + + +---- +v1.0 +---- + +[jan] Add Brazilian Portuguese translation (Fabio Gomes + ). + + +-------- +v1.0-RC2 +-------- + +[jan] Add Czech translation (Pavel Chytil ). +[cjh] Show menu in Script view (Bug #843). + + +-------- +v1.0-RC1 +-------- + +[cjh] Update icons. +[jan] Disable Kolab backends if disabled globally. + + +--------- +v1.0-BETA +--------- + +[jan] Add Spanish translation (Manuel Perez Ayala ). +[jan] Add vacation support to procmail driver (Micha Kersloot + ). + + +---------- +v1.0-ALPHA +---------- + +[mms] Add configuration option to limit total number of blacklist/whitelist + entries a user can have. +[jan] Allow setting up a global user in the backends configuration (Martin + Luethi ). +[mms] Allow user to deactivate script. +[mms] Add support to filter by body contents for IMAP driver. +[mms] Allow user-defined filters for the procmail driver (Ben Chavet + ). +[mms] Allow user-specified Headers to be used for filtering. +[mms] The storage system has been abstracted out to allow for multiple + backend storage drivers. +[mms] Removed all 'show_*' preferences - all available actions will be + shown on the menu by default. +[mms] Added the forward action (Todd Merritt ). +[mms] On demand filters can now apply to exclusively seen or unseen messages. +[mms] Rules can now be disabled (Todd Merritt ). +[mms] Added relational, regex, and matches tests to Sieve script + (Todd Merritt ). +[mms] Honor other module's handling of blacklist/whitelist. +[mms] Don't show script options if the underlying Ingo_Script:: driver + doesn't use them. +[mms] Case sensitive searches only allowed for Ingo_Script:: drivers that + can handle them. +[mms] 'blacklist_folder' preference now handled inside the 'blacklist' + preference. +[mms] Added 'show_filter_msg' preference - for use with Horde_Script:: + backends that support on demand filtering. +[mms] Added the 'mail/canApplyFilters' API call. +[mms] Ingo_Storage:: now handles all session caching. +[mms] Added IMAP client side Ingo_Script:: driver. + + +---- +v0.1 +---- + +[jan] Add Swedish translation (Anders Norrbring ). +[jan] Add Lithuanian translation (Vilius Sumskas ). +[jan] Add Italian translation (Marko Djukic ). +[jan] Add Hungarian translation (Attila Nagy ). +[mms] Add Dutch translation (Ruben van der Steenhoven + ). +[jan] Add Finnish translation (Leena Heino ). +[mms] Added the 'mail/applyFilters' API call. +[bjn] Add whitelist functions. +[jan] Add German translation. +[mms] Blacklisted addresses now stored in the storage backend in array format. +[mms] Added Ingo_Storage:: driver to allow for storage of rules in + various backends. +[bjn] Abstract Script class, and procmail script support. +[jan] Add Polish translation (Przemyslaw "Primo" Witek ). +[jan] Add Romanian translation (Eugen Hoanca ). +[jan] Let the users select what should happen to emails from blacklisted + addresses. +[jan] Add Traditional Chinese translation (David Chang + ). +[jan] Add French translation (Florent Aide ). +[jan] Add an API for Ingo. +[mac] Add a user preference to automatically update the script after a change. +[mac] Add a user preference to hide the script icon. +[mac] Fix some regex in the script generation +[jan] Default to first backend if no preferred backend is specified/found. +[mac] Initial Commit. diff --git a/ingo/docs/CREDITS b/ingo/docs/CREDITS new file mode 100644 index 000000000..fca0854f9 --- /dev/null +++ b/ingo/docs/CREDITS @@ -0,0 +1,82 @@ +======================= + Ingo Development Team +======================= + + +Core Developers +=============== + +- Michael Slusarz +- Jan Schneider + + +Localization +============ + +===================== ====================================================== +Basque Euskal Herriko Unibertsitatea +Brazilian Portuguese Fabio Gomes + Luis Felipe Marzagao + Eduardo de Carli +Catalan Jordi Giralt +Chinese (Simplified) Anna Chen +Chinese (Traditional) David Chang +Czech Pavel Chytil +Danish Brian Truelsen +Dutch Ruben van der Steenhoven + +Estonian Toomas Aas + Alar Sing +Finnish Leena Heino +French Florent Aide + Benoit St-André + Pierre Lachance + Vincent Vinet + Yannick Sebastia +German Jan Schneider +Greek Konstantinos C. Milosis +Hungarian Attila Nagy + Laszlo L. Tornoci +Italian Marko Djukic + Marco Pirovano + Cristian Manoni +Japanese Hiromi Kimura +Korean Jinhyok Heo +Latvian Janis Eisaks +Lithuanian Vilius Šumskas +Norwegian Bokmaal Trond Bjørstad + Thomas Chr. Dahl +Polish Przemyslaw "Primo" Witek + Krzysztof Kozlowski + Piotr Adamcio + Tadeusz Lesiecki + Piotr Tarnowski +Portuguese Manuel Menezes de Sequeira +Romanian Eugen Hoanca +Russian Alexey Zakharov +Slovak Martin Matuška +Slovenian Duck +Spanish Manuel Perez Ayala +Swedish Anders Norrbring +Turkish Middle East Technical University +Ukrainian Andriy Kopystyansky +===================== ====================================================== + + +Inactive Developers +=================== + +- Mike Cochrane +- Brent J. Nordquist + + +Other Thanks +============ + +Ryan Gallagher + +- The name :-) "Mail comes 'in'...Where does it 'go'?" + +Tufts University + +- Funding support for LDAP/Sieve (Sun JES/ONE & iPlanet messaging servers). diff --git a/ingo/docs/INSTALL b/ingo/docs/INSTALL new file mode 100644 index 000000000..2b02ba8de --- /dev/null +++ b/ingo/docs/INSTALL @@ -0,0 +1,211 @@ +===================== + Installing Ingo 2.0 +===================== + +:Contact: ingo@lists.horde.org + +.. contents:: Contents +.. section-numbering:: + +This document contains instructions for installing the Ingo Email Filter Rules +Manager. + +For information on the capabilities and features of IMP, see the file README_ +in the top-level directory of the IMP distribution. + + +Obtaining Ingo +============== + +Ingo can be obtained from the Horde website and FTP server, at + + http://www.horde.org/ingo/ + + ftp://ftp.horde.org/pub/ingo/ + +Or use the mirror closest to you: + + http://www.horde.org/mirrors.php + +Bleeding-edge development versions of Ingo are available via Git; see the file +`horde/docs/HACKING`_, or the website http://www.horde.org/source/, for +information on accessing the Horde Git repository. + + +Prerequisites +============= + +To function properly, Ingo **requires** the following: + +1. A working Horde installation + + Ingo runs within the `Horde Application Framework`_, a set of common tools + for Web applications written in PHP. You must install Horde before + installing Ingo. + + .. Important:: Ingo 2.0 requires version 4.0+ of the Horde Framework - + earlier versions of Horde will **not** work. + + .. _`Horde Application Framework`: http://www.horde.org/horde/ + + The Horde Framework can be obtained from the Horde website and FTP server, + at + + http://www.horde.org/horde/ + + ftp://ftp.horde.org/pub/horde/ + + Many of Ingo's prerequisites are also Horde prerequisites. + + .. Important:: Be sure to have completed all of the steps in the + `horde/docs/INSTALL`_ file for the Horde Framework before + installing Ingo. + +2. The following PEAR modules: + (See `horde/docs/INSTALL`_ for instructions on installing PEAR modules) + + a. Net_Sieve 1.0.1 [OPTIONAL] + + Ingo uses the Net_Sieve class for communicating with timsieved running + on Cyrus mail servers. You will only need to install this class if you + are using Sieve for filtering. + + b. Net_Socket [OPTIONAL] + + Net_Socket is used by Net_Sieve and, thus, is only requires if you will + be using Sieve filtering. PEAR *should* install Net_Socket when it + installs Net_Sieve. + + +Installing Ingo +=============== + +Ingo is written in PHP, and must be installed in a web-accessible directory. +The precise location of this directory will differ from system to system. +Conventionally, Ingo is installed directly underneath Horde in the webserver's +document tree. + +Since Ingo is written in PHP, there is no compilation necessary; simply expand +the distribution where you want it to reside and rename the root directory of +the distribution to whatever you wish to appear in the URL. For example, with +the Apache webserver's default document root of ``/usr/local/apache/htdocs``, +you would type:: + + cd /usr/local/apache/htdocs/horde + tar zxvf /path/to/ingo-x.y.z.tar.gz + mv ingo-x.y.z ingo + +and would then find Ingo at the URL:: + + http://your-server/horde/ingo/ + + +Configuring Ingo +================ + +1. Configuring Horde for Ingo + + a. Register the application + + In ``horde/config/registry.php``, find the applications['ingo'] stanza. + The default settings here should be okay, but you can change them if + desired. If you have changed the location of Ingo relative to Horde, + either in the URL, in the filesystem or both, you must update the + ``fileroot`` and ``webroot`` settings to their correct values. + +2. Creating the database table + + The specific steps to create the Ingo database table depend on which + database you've chosen to use. + + First, look in ``scripts/sql/`` to see if a script already exists + for your database type. If so, you should be able to simply execute that + script as superuser in your database. (Note that executing the script as + the "horde" user will probably fail when granting privileges.) + + If such a script does not exist, you'll need to build your own, using the + file ingo.sql as a starting point. If you need assistance in creating + databases, you may wish to let us know on the IMP mailing list. + +3. Configuring Ingo + + To configure Ingo, change to the ``config/`` directory of the installed + distribution, and make copies of all of the configuration ``dist`` files + without the ``dist`` suffix:: + + cd config/ + for foo in *.dist; do cp $foo `basename $foo .dist`; done + + Or on Windows:: + + copy *.dist *. + + Documentation on the format of those files can be found in each file. With + the exception of the ``conf.*`` files (see below) and ``backends.php``, the + other files in ``config/`` need only be modified if you wish to customize + Ingo's appearance or behavior, as the defaults will be correct for most + sites. + + You must login to Horde as a Horde Administrator to finish the + configuration of Ingo. Use the Horde ``Administration`` menu item to get + to the administration page, and then click on the ``Configuration`` icon to + get the configuration page. Select ``Filters`` from the selection list of + applications. Fill in or change any configuration values as needed. When + done click on ``Generate Filters Configuration`` to generate the + ``conf.php`` file. If your web server doesn't have write permissions to + the Ingo configuration directory or file, it will not be able to write the + file. In this case, go back to ``Configuration`` and choose one of the + other methods to create the configuration file ``ingo/config/conf.php``. + + If you want to disable any of the special rules like blacklist, vacation, + or spam, you can lock the according preferences in + ``ingo/config/prefs.php``. + + Note for international users: Ingo uses GNU gettext to provide local + translations of text displayed by applications; the translations are found + in the po/ directory. If a translation is not yet available for your + locale (and you wish to create one), see the ``horde/po/README`` file, or + if you're having trouble using a provided translation, please see the + `horde/docs/TRANSLATIONS`_ file for instructions. + + +Known Problems +============== + +Some people experienced problems with Ingo's Sieve driver and Cyrus 2.1.12. +The "addflag/removeflag" Sieve commands can cause message copies sticking in +Cyrus' message queues producing "virtual memory exhausted" errors. It was +reported that upgrading to version 2.1.15 has fixed this problem. + + +Obtaining Support +================= + +If you encounter problems with Ingo, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users may also be found on IRC, +on the channel #horde on the Freenode Network (irc.freenode.net). + +Please keep in mind that Ingo is free software written by volunteers. +For information on reasonable support expectations, please read + + http://www.horde.org/support.php + +Thanks for using Ingo! + +The Ingo team + + +.. _`horde/docs/HACKING`: ../../horde/docs/?f=HACKING.html +.. _`horde/docs/INSTALL`: ../../horde/docs/?f=INSTALL.html +.. _`horde/docs/TRANSLATIONS`: ../../horde/docs/?f=TRANSLATIONS.html diff --git a/ingo/docs/RELEASE_NOTES b/ingo/docs/RELEASE_NOTES new file mode 100644 index 000000000..d2dc1f482 --- /dev/null +++ b/ingo/docs/RELEASE_NOTES @@ -0,0 +1,48 @@ +notes['fm']['focus'] = 4; + +/* Mailing list release notes. */ +$this->notes['ml']['changes'] = <<notes['fm']['changes'] = <<notes['name'] = 'Ingo'; +$this->notes['fm']['project'] = 'ingo'; +$this->notes['fm']['branch'] = 'Default'; diff --git a/ingo/docs/TODO b/ingo/docs/TODO new file mode 100644 index 000000000..da15cb971 --- /dev/null +++ b/ingo/docs/TODO @@ -0,0 +1,10 @@ +============================ + Ingo Development TODO List +============================ + +- Better checks for bulk and mailing lists in vacation. + +- A smart composite driver that would let you define seperate drivers for + forwards, vacation, and filters. Should also define the order of those + backends so that Ingo only allows re-arranging of rule order within the + correct bounds. diff --git a/ingo/docs/UPGRADING b/ingo/docs/UPGRADING new file mode 100644 index 000000000..9e22dc99c --- /dev/null +++ b/ingo/docs/UPGRADING @@ -0,0 +1,89 @@ +================ + Upgrading Ingo +================ + +:Contact: ingo@lists.horde.org + + +These are instructions to upgrade from earlier Ingo versions. Please +backup your existing data before running any of the steps described below. + + +Upgrading Ingo From 1.2.1 To 1.2.2 +================================== + +The group_uid field in the SQL share driver groups table has been changed from +an INT to a VARCHAR(255). Execute the provided SQL script to update your +database if you are using the native SQL share driver. + + mysql --user=root --password= < 1.2.1_to_1.2.2.sql + + +Upgrading Ingo From 1.2 To 1.2.1 +================================ + +The share_owner field in the SQL share driver table has been changed from a +VARCHAR(32) to a VARCHAR(255). Execute the provided SQL script to update your +database if you are using the native SQL share driver. + + mysql --user=root --password= < 1.2_to_1.2.1.sql + + +Upgrading Ingo From 1.1.x To 1.2 +================================== + + +This is a non-exhaustive, quick explanation of what has changed between Ingo +version 1.1.x and 1.2.x. + + +SQL Backend +----------- + +An SQL table has been added than can optionally be used as a storage backend +for the filter rules. Using this backend no longer limits the number and size +of rules. + +Execute the provided SQL script to add the table to your database, e.g.:: + + mysql --user=root --password= < scripts/sql/ingo.sql + +You also have to execute the provided PHP script to migrate the existing rules +from the preferences backend to the new database table:: + + php scripts/upgrades/convert_prefs_to_sql.php < filename + +``filename`` is a file that contains a list of users, one username per line. +The username should be the same as how the preferences are stored in the +preferences backend (e.g. usernames may have to be in the form +user@example.com). You can create such a list with the following MySQL +command:: + + mysql --user=root --password= --skip-column-names --batch --execute='SELECT DISTINCT pref_uid FROM horde_prefs' + + +New Beta SQL Share Driver Support +--------------------------------- + +A new beta-level SQL Horde_Share driver has been added in Horde 3.2. This driver +offers significant performance improvements over the existing Datatree driver, +but it has not received the same level of testing, thus the beta designation. +In order to make use of this driver, you must be using Horde 3.2-RC3 or +later. To migrate your existing share data, run +``convert_datatree_shares_to_sql.php``. Be sure to read the entry above and create +the new SQL tables before running the migration script. + + +Upgrading Ingo From 1.0.x To 1.1.x +================================== + +This is a non-exhaustive, quick explanation of what has changed between Ingo +version 1.0.x and 1.1.x. + + +Backends parameter changes - procmail driver +-------------------------------------------- + +In ``config/backends.php``, the ``procmailrc`` parameter in the procmail +entry has been deprecated. It has been replaced by the ``filename`` +parameter. diff --git a/ingo/filters.php b/ingo/filters.php new file mode 100644 index 000000000..ef8cc6559 --- /dev/null +++ b/ingo/filters.php @@ -0,0 +1,322 @@ + + */ + +require_once dirname(__FILE__) . '/lib/base.php'; + +/* Get the list of filter rules. */ +$filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); +if (is_a($filters, 'PEAR_Error')) { + Horde::fatal($filters, __FILE__, __LINE__); +} + +/* Load the Ingo_Script:: driver. */ +$ingo_script = Ingo::loadIngoScript(); + +/* Determine if we need to show the on-demand settings. */ +$on_demand = $ingo_script->performAvailable(); + +/* Get web parameter data. */ +$actionID = Util::getFormData('actionID'); +$id = Util::getFormData('rulenumber'); + +/* Get permissions. */ +$edit_allowed = Ingo::hasPermission('shares', PERMS_EDIT); +$delete_allowed = Ingo::hasPermission('shares', PERMS_DELETE); + +/* Perform requested actions. */ +switch ($actionID) { +case 'rule_down': +case 'rule_up': +case 'rule_copy': +case 'rule_delete': +case 'rule_disable': +case 'rule_enable': + if (!$edit_allowed) { + $notification->push(_("You do not have permission to edit filter rules."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; + } + switch ($actionID) { + case 'rule_delete': + if (!$delete_allowed) { + $notification->push(_("You do not have permission to delete filter rules."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; + } + if ($filters->deleteRule($id)) { + $notification->push(_("Rule Deleted"), 'horde.success'); + } + break; + + case 'rule_copy': + if (!Ingo::hasPermission('allow_rules')) { + $message = @htmlspecialchars(_("You are not allowed to create or edit custom rules."), ENT_COMPAT, NLS::getCharset()); + if (!empty($conf['hooks']['permsdenied'])) { + $message = Horde::callHook('_perms_hook_denied', array('ingo:allow_rules'), 'horde', $message); + } + $notification->push($message, 'horde.error', array('content.raw')); + break 2; + } elseif (Ingo::hasPermission('max_rules') !== true && + Ingo::hasPermission('max_rules') <= count($filters->getFilterList())) { + $message = @htmlspecialchars(sprintf(_("You are not allowed to create more than %d rules."), Ingo::hasPermission('max_rules')), ENT_COMPAT, NLS::getCharset()); + if (!empty($conf['hooks']['permsdenied'])) { + $message = Horde::callHook('_perms_hook_denied', array('ingo:max_rules'), 'horde', $message); + } + $notification->push($message, 'horde.error', array('content.raw')); + break 2; + } elseif ($filters->copyRule($id)) { + $notification->push(_("Rule Copied"), 'horde.success'); + } + break; + + case 'rule_up': + $steps = Util::getFormData('steps', 1); + $filters->ruleUp($id, $steps); + break; + + case 'rule_down': + $steps = Util::getFormData('steps', 1); + $filters->ruleDown($id, $steps); + break; + + case 'rule_disable': + $filters->ruleDisable($id); + $notification->push(_("Rule Disabled"), 'horde.success'); + break; + + case 'rule_enable': + $filters->ruleEnable($id); + $notification->push(_("Rule Enabled"), 'horde.success'); + break; + } + + /* Save changes */ + $ingo_storage->store($filters); + if ($prefs->getValue('auto_update')) { + Ingo::updateScript(); + } + break; + +case 'settings_save': + if (!$edit_allowed) { + $notification->push(_("You do not have permission to edit filter rules."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; + } + $prefs->setValue('show_filter_msg', Util::getFormData('show_filter_msg')); + $prefs->setValue('filter_seen', Util::getFormData('filter_seen')); + $notification->push(_("Settings successfully updated."), 'horde.success'); + break; + +case 'apply_filters': + if (!$edit_allowed) { + $notification->push(_("You do not have permission to edit filter rules."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; + } + if ($ingo_script->canApply()) { + $ingo_script->apply(); + } + break; +} + +/* Get the list of rules now. */ +$filter_list = $filters->getFilterList(); + +Horde::addScriptFile('prototype.js', 'horde', true); +Horde::addScriptFile('tooltip.js', 'horde', true); +Horde::addScriptFile('stripe.js', 'horde', true); +$title = _("Filter Rules"); +require INGO_TEMPLATES . '/common-header.inc'; +require INGO_TEMPLATES . '/menu.inc'; +require INGO_TEMPLATES . '/filters/header.inc'; + +/* Common URLs. */ +$filters_url = Horde::applicationUrl('filters.php'); +$rule_url = Horde::applicationUrl('rule.php'); + +if (count($filter_list) == 0) { + require INGO_TEMPLATES . '/filters/filter-none.inc'; +} else { + $display = array(); + $i = 0; + $rule_count = array_sum(array_map(create_function('$a', "return (in_array(\$a['action'], \$_SESSION['ingo']['script_categories'])) ? 1 : 0;"), $filter_list)); + + /* Common graphics. */ + $down_img = Horde::img('nav/down.png', _("Move Rule Down"), '', $registry->getImageDir('horde')); + $up_img = Horde::img('nav/up.png', _("Move Rule Up"), '', $registry->getImageDir('horde')); + + foreach ($filter_list as $rule_number => $filter) { + /* Skip non-display categories. */ + if (!in_array($filter['action'], $_SESSION['ingo']['script_categories'])) { + continue; + } + + $entry = array(); + $entry['number'] = ++$i; + $url = Util::addParameter($filters_url, 'rulenumber', $rule_number); + $copyurl = $delurl = $editurl = $name = null; + + switch ($filter['action']) { + case Ingo_Storage::ACTION_BLACKLIST: + $editurl = Horde::applicationUrl('blacklist.php'); + $entry['filterimg'] = Horde::img('blacklist.png'); + $name = _("Blacklist"); + break; + + case Ingo_Storage::ACTION_WHITELIST: + $editurl = Horde::applicationUrl('whitelist.php'); + $entry['filterimg'] = Horde::img('whitelist.png'); + $name = _("Whitelist"); + break; + + case Ingo_Storage::ACTION_VACATION: + $editurl = Horde::applicationUrl('vacation.php'); + $entry['filterimg'] = Horde::img('vacation.png'); + $name = _("Vacation"); + break; + + case Ingo_Storage::ACTION_FORWARD: + $editurl = Horde::applicationUrl('forward.php'); + $entry['filterimg'] = Horde::img('forward.png'); + $name = _("Forward"); + break; + + case Ingo_Storage::ACTION_SPAM: + $editurl = Horde::applicationUrl('spam.php'); + $entry['filterimg'] = Horde::img('spam.png'); + $name = _("Spam Filter"); + break; + + default: + $editurl = Util::addParameter($rule_url, array('edit' => $rule_number, 'actionID' => 'rule_edit')); + $delurl = Util::addParameter($url, 'actionID', 'rule_delete'); + $copyurl = Util::addParameter($url, 'actionID', 'rule_copy'); + $entry['filterimg'] = false; + $name = $filter['name']; + break; + } + + /* Create description. */ + if (!$edit_allowed) { + $entry['descriplink'] = @htmlspecialchars($name, ENT_COMPAT, NLS::getCharset()); + } elseif (!empty($filter['conditions'])) { + $descrip = ''; + $condition_size = count($filter['conditions']) - 1; + foreach ($filter['conditions'] as $condid => $cond) { + + $descrip .= sprintf("%s %s \"%s\"", _($cond['field']), _($cond['match']), $cond['value']); + if (!empty($cond['case'])) { + $descrip .= ' [' . _("Case Sensitive") . ']'; + } + if ($condid < $condition_size) { + $descrip .= ($filter['combine'] == Ingo_Storage::COMBINE_ALL) ? _(" and") : _(" or"); + $descrip .= "\n "; + } + } + + $descrip .= "\n"; + + $ob = $ingo_storage->getActionInfo($filter['action']); + $descrip .= $ob->label; + + if ($filter['action-value']) { + $descrip .= ': ' . $filter['action-value']; + } + + if ($filter['stop']) { + $descrip .= "\n[stop]"; + } + + $entry['descriplink'] = Horde::linkTooltip($editurl, sprintf(_("Edit %s"), $name), null, null, null, $descrip) . @htmlspecialchars($name, ENT_COMPAT, NLS::getCharset()) . ''; + } else { + $entry['descriplink'] = Horde::link($editurl, sprintf(_("Edit %s"), $name)) . @htmlspecialchars($name, ENT_COMPAT, NLS::getCharset()) . ''; + } + + /* Create edit link. */ + $entry['editlink'] = Horde::link($editurl, sprintf(_("Edit %s"), $name)); + $entry['editimg'] = Horde::img('edit.png', sprintf(_("Edit %s"), $name), '', $registry->getImageDir('horde')); + + /* Create delete link. */ + if (!is_null($delurl)) { + $entry['dellink'] = Horde::link($delurl, sprintf(_("Delete %s"), $name), null, null, "return window.confirm('" . addslashes(_("Are you sure you want to delete this rule?")) . "');"); + $entry['delimg'] = Horde::img('delete.png', sprintf(_("Delete %s"), $name), '', $registry->getImageDir('horde')); + } else { + $entry['dellink'] = false; + } + + /* Create copy link. */ + if (!is_null($copyurl) && + (!empty($conf['hooks']['permsdenied']) || + Ingo::hasPermission('max_rules') === true || + Ingo::hasPermission('max_rules') > count($filter_list))) { + $entry['copylink'] = Horde::link($copyurl, sprintf(_("Copy %s"), $name)); + $entry['copyimg'] = Horde::img('copy.png', sprintf(_("Copy %s"), $name)); + } else { + $entry['copylink'] = false; + } + + /* Create up/down arrow links. */ + $entry['upurl'] = Util::addParameter($url, 'actionID', 'rule_up'); + $entry['downurl'] = Util::addParameter($url, 'actionID', 'rule_down'); + $entry['uplink'] = ($i > 1) ? Horde::link($entry['upurl'], _("Move Rule Up")) : false; + $entry['downlink'] = ($i < $rule_count) ? Horde::link($entry['downurl'], _("Move Rule Down")) : false; + + if (empty($filter['disable'])) { + if ($edit_allowed) { + $entry['disablelink'] = Horde::link(Util::addParameter($url, 'actionID', 'rule_disable'), sprintf(_("Disable %s"), $name)); + $entry['disableimg'] = Horde::img('enable.png', sprintf(_("Disable %s"), $name)); + } else { + $entry['disableimg'] = Horde::img('enable.png'); + $entry['disablelink'] = false; + } + $entry['enablelink'] = false; + $entry['enableimg'] = false; + } else { + if ($edit_allowed) { + $entry['enablelink'] = Horde::link(Util::addParameter($url, 'actionID', 'rule_enable'), sprintf(_("Enable %s"), $name)); + $entry['enableimg'] = Horde::img('disable.png', sprintf(_("Enable %s"), $name)); + } else { + $entry['enableimg'] = Horde::img('disable.png'); + $entry['enablelink'] = false; + } + $entry['disablelink'] = false; + $entry['disableimg'] = false; + } + + $display[] = $entry; + } + + /* Output the template. */ + $template = new Ingo_Template(); + $template->set('down_img', $down_img); + $template->set('up_img', $up_img); + $template->set('filter', $display, true); + $template->set('edit_allowed', $edit_allowed, true); + $template->set('delete_allowed', $delete_allowed, true); + $template->setOption('gettext', true); + echo $template->fetch(INGO_TEMPLATES . '/filters/filter.html'); +} + +$actions = $ingo_script->availableActions(); +$createrule = (!empty($actions) && + (!empty($conf['hooks']['permsdenied']) || + (Ingo::hasPermission('allow_rules') && + (Ingo::hasPermission('max_rules') === true || + Ingo::hasPermission('max_rules') > count($filter_list))))); +$canapply = $ingo_script->canApply(); +require INGO_TEMPLATES . '/filters/footer.inc'; +if ($on_demand && $edit_allowed) { + require INGO_TEMPLATES . '/filters/settings.inc'; +} + +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/ingo/forward.php b/ingo/forward.php new file mode 100644 index 000000000..029f458c5 --- /dev/null +++ b/ingo/forward.php @@ -0,0 +1,105 @@ + + */ + +require_once dirname(__FILE__) . '/lib/base.php'; + +/* Redirect if forward is not available. */ +if (!in_array(Ingo_Storage::ACTION_FORWARD, $_SESSION['ingo']['script_categories'])) { + $notification->push(_("Forward is not supported in the current filtering driver."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; +} + +/* Get the forward object and rule. */ +$forward = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FORWARD); +$filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); +$fwd_id = $filters->findRuleId(Ingo_Storage::ACTION_FORWARD); +$fwd_rule = $filters->getRule($fwd_id); + +/* Load libraries. */ +require_once 'Horde/Variables.php'; +$vars = &Variables::getDefaultVariables(); +if ($vars->get('submitbutton') == _("Return to Rules List")) { + header('Location: ' . Horde::applicationUrl('filters.php', true)); + exit; +} + +/* Build form. */ +$form = new Horde_Form($vars); +$v = &$form->addVariable(_("Keep a copy of messages in this account?"), 'keep_copy', 'boolean', false); +$v->setHelp('forward-keepcopy'); +$v = &$form->addVariable(_("Address(es) to forward to:"), 'addresses', 'longtext', false, false, null, array(5, 40)); +$v->setHelp('forward-addresses'); +$form->setButtons(_("Save")); + +/* Perform requested actions. */ +if ($form->validate($vars)) { + $forward->setForwardAddresses($vars->get('addresses')); + $forward->setForwardKeep($vars->get('keep_copy') == 'on'); + $success = true; + if (is_a($result = $ingo_storage->store($forward), 'PEAR_Error')) { + $notification->push($result); + $success = false; + } else { + $notification->push(_("Changes saved."), 'horde.success'); + if ($vars->get('submitbutton') == _("Save and Enable")) { + $filters->ruleEnable($fwd_id); + if (is_a($result = $ingo_storage->store($filters), 'PEAR_Error')) { + $notification->push($result); + $success = false; + } else { + $notification->push(_("Rule Enabled"), 'horde.success'); + $fwd_rule['disable'] = false; + } + } elseif ($vars->get('submitbutton') == _("Save and Disable")) { + $filters->ruleDisable($fwd_id); + if (is_a($result = $ingo_storage->store($filters), 'PEAR_Error')) { + $notification->push($result); + $success = false; + } else { + $notification->push(_("Rule Disabled"), 'horde.success'); + $fwd_rule['disable'] = true; + } + } + } + if ($success && $prefs->getValue('auto_update')) { + Ingo::updateScript(); + } +} + +/* Add buttons depending on the above actions. */ +if (empty($fwd_rule['disable'])) { + $form->appendButtons(_("Save and Disable")); +} else { + $form->appendButtons(_("Save and Enable")); +} +$form->appendButtons(_("Return to Rules List")); + +/* Set default values. */ +if (!$form->isSubmitted()) { + $vars->set('keep_copy', $forward->getForwardKeep()); + $vars->set('addresses', implode("\n", $forward->getForwardAddresses())); +} + +/* Set form title. */ +$form_title = _("Forward"); +if (!empty($fwd_rule['disable'])) { + $form_title .= ' [' . _("Disabled") . ']'; +} +$form_title .= ' ' . Help::link('ingo', 'forward'); +$form->setTitle($form_title); + +$title = _("Forwards Edit"); +require INGO_TEMPLATES . '/common-header.inc'; +require INGO_TEMPLATES . '/menu.inc'; +$form->renderActive(new Horde_Form_Renderer(array('encode_title' => false)), $vars, 'forward.php', 'post'); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/ingo/index.php b/ingo/index.php new file mode 100644 index 000000000..bf9c3e0f6 --- /dev/null +++ b/ingo/index.php @@ -0,0 +1,26 @@ + + */ + +@define('INGO_BASE', dirname(__FILE__)); +$ingo_configured = (is_readable(INGO_BASE . '/config/conf.php') && + is_readable(INGO_BASE . '/config/prefs.php') && + is_readable(INGO_BASE . '/config/backends.php') && + is_readable(INGO_BASE . '/config/fields.php')); + +if (!$ingo_configured) { + require INGO_BASE . '/../lib/Test.php'; + Horde_Test::configFilesMissing('Ingo', INGO_BASE, + array('conf.php', 'prefs.php', 'backends.php'), + array('fields.php' => 'This file defines types of credentials that a backend might request.')); +} + +require INGO_BASE . '/filters.php'; diff --git a/ingo/lib/Block/overview.php b/ingo/lib/Block/overview.php new file mode 100644 index 000000000..d92bd287f --- /dev/null +++ b/ingo/lib/Block/overview.php @@ -0,0 +1,111 @@ + + * @package Horde_Block + */ +class Horde_Block_ingo_overview extends Horde_Block { + + var $_app = 'ingo'; + + /** + * The title to go in this block. + * + * @return string The title text. + */ + function _title() + { + return Horde::link(Horde::url($GLOBALS['registry']->getInitialPage(), true)) . $GLOBALS['registry']->get('name') . ''; + } + + /** + * The content to go in this block. + * + * @return string The content + */ + function _content() + { + require_once dirname(__FILE__) . '/../base.php'; + + /* Get list of filters */ + $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS); + $html = ''; + $html_pre = ''; + foreach ($filters->_filters as $filter) { + if (!empty($filter['disable'])) { + $active = _("inactive"); + } else { + $active = _("active"); + } + + switch($filter['name']) { + case 'Vacation': + if (in_array(Ingo_Storage::ACTION_VACATION, $_SESSION['ingo']['script_categories'])) { + $html .= $html_pre . + Horde::img('vacation.png', _("Vacation")) . + '
'; + $html_post = '
' . + Horde::link(Horde::applicationUrl('vacation.php'), _("Edit")) . + _("Vacation") . ' ' . $active . $html_post; + } + break; + + case 'Forward': + if (in_array(Ingo_Storage::ACTION_FORWARD, $_SESSION['ingo']['script_categories'])) { + $html .= $html_pre . + Horde::img('forward.png', _("Forward")) . '' . + Horde::link(Horde::applicationUrl('forward.php'), _("Edit")) . + _("Forward") . ' ' . $active; + $data = unserialize($GLOBALS['prefs']->getValue('forward')); + if (!empty($data['a'])) { + $html .= ':
' . implode('
', $data['a']); + } + $html .= $html_post; + } + break; + + case 'Whitelist': + if (in_array(Ingo_Storage::ACTION_WHITELIST, $_SESSION['ingo']['script_categories'])) { + $html .= $html_pre . + Horde::img('whitelist.png', _("Whitelist")) . + '
' . + Horde::link(Horde::applicationUrl('whitelist.php'), _("Edit")) . + _("Whitelist") . ' ' . $active . $html_post; + } + break; + + case 'Blacklist': + if (in_array(Ingo_Storage::ACTION_BLACKLIST, $_SESSION['ingo']['script_categories'])) { + $html .= $html_pre . + Horde::img('blacklist.png', _("Blacklist")) . + '' . + Horde::link(Horde::applicationUrl('blacklist.php'), _("Edit")) . + _("Blacklist") . ' ' . $active . $html_post; + } + break; + + case 'Spam Filter': + if (in_array(Ingo_Storage::ACTION_SPAM, $_SESSION['ingo']['script_categories'])) { + $html .= $html_pre . + Horde::img('spam.png', _("Spam Filter")) . + '' . + Horde::link(Horde::applicationUrl('spam.php'), _("Edit")) . + _("Spam Filter") . ' ' . $active . $html_post; + } + break; + } + + } + + return $html . '
'; + } + +} diff --git a/ingo/lib/Driver.php b/ingo/lib/Driver.php new file mode 100644 index 000000000..a972abb82 --- /dev/null +++ b/ingo/lib/Driver.php @@ -0,0 +1,82 @@ + + * @package Ingo + */ +class Ingo_Driver +{ + /** + * Driver specific parameters + * + * @var array + */ + protected $_params = array( + 'username' => null, + 'password' => null + ); + + /** + * Whether this driver allows managing other users' rules. + * + * @var boolean + */ + protected $_support_shares = false; + + /** + * Attempts to return a concrete Ingo_Driver instance based on $driver. + * + * @param string $driver The type of concrete Ingo_Driver subclass to + * return. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return mixed The newly created concrete Ingo_Driver instance, or + * false on error. + */ + static public function factory($driver, $params = array()) + { + $driver = basename($driver); + $class = 'Ingo_Driver_' . $driver; + + return class_exists($class) + ? new $class($params) + : false; + } + + /** + * Constructor. + */ + public function __construct($params = array()) + { + $this->_params = array_merge($this->_params, $params); + } + + /** + * Sets a script running on the backend. + * + * @param string $script The filter script. + * + * @return mixed True on success, false if script can't be activated. + * Returns PEAR_Error on error. + */ + public function setScriptActive($script) + { + return false; + } + + /** + * Returns whether the driver supports managing other users' rules. + * + * @return boolean True if the driver supports shares. + */ + public function supportShares() + { + return $this->_support_shares && !empty($_SESSION['ingo']['backend']['shares']); + } + +} diff --git a/ingo/lib/Driver/ldap.php b/ingo/lib/Driver/ldap.php new file mode 100644 index 000000000..75f75044e --- /dev/null +++ b/ingo/lib/Driver/ldap.php @@ -0,0 +1,263 @@ + + * @package Ingo + */ +class Ingo_Driver_ldap extends Ingo_Driver +{ + /** + * Constructor. + */ + public function __construct($params = array()) + { + if (!Util::extensionExists('ldap')) { + Horde::fatal(PEAR::raiseError(_("LDAP support is required but the LDAP module is not available or not loaded.")), __FILE__, __LINE__); + } + + $default_params = array( + 'hostspec' => 'localhost', + 'port' => 389, + 'script_attribute' => 'mailSieveRuleSource' + ); + + parent::__construct(array_merge($default_params, $params)); + } + + /** + * Create a DN from a DN template. + * This is done by substituting the username for %u and the 'dc=' + * components for %d. + * + * @param string $templ The DN template (from the config). + * + * @return string The resulting DN. + */ + protected function _substUser($templ) + { + $domain = ''; + $username = $this->_params['username']; + + if (strpos($username, '@') !== false) { + list($username, $domain) = explode('@', $username); + } + $domain = implode(', dc=', explode('.', $domain)); + if (!empty($domain)) { + $domain = 'dc=' . $domain; + } + + if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $username)) { + $username = '"' . str_replace('"', '\\"', $username) . '"'; + } + + return str_replace(array('%u', '%d'), + array($username, $domain), + $templ); + } + + /** + * Connect and bind to ldap server. + */ + protected function _connect() + { + if (!($ldapcn = @ldap_connect($this->_params['hostspec'], + $this->_params['port']))) { + return PEAR::raiseError(_("Connection failure")); + } + + /* Set the LDAP protocol version. */ + if (!empty($this->_params['version'])) { + @ldap_set_option($ldapcn, + LDAP_OPT_PROTOCOL_VERSION, + $this->_params['version']); + } + + /* Start TLS if we're using it. */ + if (!empty($this->_params['tls'])) { + if (!@ldap_start_tls($ldapcn)) { + return PEAR::raiseError(sprintf(_("STARTTLS failed: (%s) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + } + + /* Bind to the server. */ + if (isset($this->_params['bind_dn'])) { + $bind_dn = $this->_substUser($this->_params['bind_dn']); + if (is_a($bind_dn, 'PEAR_Error')) { + return $bind_dn; + } + + if (isset($this->_params['bind_password'])) { + $password = $this->_params['bind_password']; + } else { + $password = $this->_params['password']; + } + + if (!@ldap_bind($ldapcn, $bind_dn, $password)) { + return PEAR::raiseError(sprintf(_("Bind failed: (%s) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + } elseif (!(@ldap_bind($ldapcn))) { + return PEAR::raiseError(sprintf(_("Bind failed: (%s) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + + return $ldapcn; + } + + /** + * Retrieve current user's scripts. + * + * @param resource $ldapcn The connection to the LDAP server. + * @param string $userDN Set to the user object's real DN. + * + * @return mixed Array of script sources, or PEAR_Error on failure. + */ + protected function _getScripts($ldapcn, &$userDN) + { + $attrs = array($this->_params['script_attribute'], 'dn'); + $filter = $this->_substUser($this->_params['script_filter']); + + /* Find the user object. */ + $sr = @ldap_search($ldapcn, $this->_params['script_base'], $filter, + $attrs); + if ($sr === false) { + return PEAR::raiseError(sprintf(_("Error retrieving current script: (%d) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + if (@ldap_count_entries($ldapcn, $sr) != 1) { + return PEAR::raiseError(sprintf(_("Expected 1 object, got %d."), + ldap_count_entries($ldapcn, $sr))); + } + $ent = @ldap_first_entry($ldapcn, $sr); + if ($ent === false) { + return PEAR::raiseError(sprintf(_("Error retrieving current script: (%d) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + + /* Retrieve the user's DN. */ + $v = @ldap_get_dn($ldapcn, $ent); + if ($v === false) { + @ldap_free_result($sr); + return PEAR::raiseError(sprintf(_("Error retrieving current script: (%d) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + $userDN = $v; + + /* Retrieve the user's scripts. */ + $attrs = @ldap_get_attributes($ldapcn, $ent); + @ldap_free_result($sr); + if ($attrs === false) { + return PEAR::raiseError(sprintf(_("Error retrieving current script: (%d) %s"), + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + + /* Attribute can be in any case, and can have a ";binary" + * specifier. */ + $regexp = '/^' . preg_quote($this->_params['script_attribute'], '/') . + '(?:;.*)?$/i'; + unset($attrs['count']); + foreach ($attrs as $name => $values) { + if (preg_match($regexp, $name)) { + unset($values['count']); + return array_values($values); + } + } + + return array(); + } + + /** + * Sets a script running on the backend. + * + * @param string $script The sieve script. + * + * @return mixed True on success, PEAR_Error on error. + */ + protected function setScriptActive($script) + { + $ldapcn = $this->_connect(); + if (is_a($ldapcn, 'PEAR_Error')) { + return $ldapcn; + } + + $values = $this->_getScripts($ldapcn, $userDN); + if (is_a($values, 'PEAR_Error')) { + return $values; + } + + $found = false; + foreach ($values as $i => $value) { + if (strpos($value, "# Sieve Filter\n") !== false) { + if (empty($script)) { + unset($values[$i]); + } else { + $values[$i] = $script; + } + $found = true; + break; + } + } + if (!$found && !empty($script)) { + $values[] = $script; + } + + $replace = array(String::lower($this->_params['script_attribute']) => $values); + if (empty($values)) { + $r = @ldap_mod_del($ldapcn, $userDN, $replace); + } else { + $r = @ldap_mod_replace($ldapcn, $userDN, $replace); + } + if (!$r) { + return PEAR::raiseError(sprintf(_("Activating the script for \"%s\" failed: (%d) %s"), + $userDN, + ldap_errno($ldapcn), + ldap_error($ldapcn))); + } + + @ldap_close($ldapcn); + return true; + } + + /** + * Returns the content of the currently active script. + * + * @return string The complete ruleset of the specified user. + */ + public function getScript() + { + $ldapcn = $this->_connect(); + if (is_a($ldapcn, 'PEAR_Error')) { + return $ldapcn; + } + + $values = $this->_getScripts($ldapcn, $userDN); + if (is_a($values, 'PEAR_Error')) { + return $values; + } + + $script = ''; + foreach ($values as $value) { + if (strpos($value, "# Sieve Filter\n") !== false) { + $script = $value; + break; + } + } + + @ldap_close($ldapcn); + return $script; + } + +} diff --git a/ingo/lib/Driver/null.php b/ingo/lib/Driver/null.php new file mode 100644 index 000000000..54cadee06 --- /dev/null +++ b/ingo/lib/Driver/null.php @@ -0,0 +1,24 @@ + + * @package Ingo + */ + +class Ingo_Driver_null extends Ingo_Driver +{ + /** + * Constructor. + */ + public function __construct($params = array()) + { + $this->_support_shares = true; + parent::__construct($params); + } + +} diff --git a/ingo/lib/Driver/sivtest.php b/ingo/lib/Driver/sivtest.php new file mode 100644 index 000000000..3fc39536a --- /dev/null +++ b/ingo/lib/Driver/sivtest.php @@ -0,0 +1,210 @@ + + * + * See the enclosed file LICENSE for license information (ASL). If you + * did not receive this file, see http://www.horde.org/licenses/asl.php. + * + * @author Mike Cochrane + * @author Jan Schneider + * @author Liam Hoekenga + * @package Ingo + */ +class Ingo_Driver_sivtest extends Ingo_Driver +{ + /** + * The Net_Sieve object. + * + * @var Net_Sieve + */ + protected $_sieve; + + /** + * Constructor. + */ + public function __construct($params = array()) + { + $default_params = array( + 'hostspec' => 'localhost', + 'logintype' => '', + 'port' => 2000, + 'scriptname' => 'ingo', + 'admin' => '', + 'usetls' => true, + 'command' => '', + 'socket' => '', + ); + + parent::__construct(array_merge($default_params, $params)); + } + + /** + * Connect to the sieve server. + * + * @return mixed True on success, PEAR_Error on false. + */ + protected function _connect() + { + if (!empty($this->_sieve)) { + return true; + } + + $this->sivtestSocket($this->_params['username'], + $this->_params['password'], $this->_params['hostspec']); + if (substr(PHP_VERSION, 0, 1) == '5') { + $domain_socket = 'unix://' . $this->_params['socket']; + } else { + $domain_socket = $this->_params['socket']; + } + + $this->_sieve = new Net_Sieve($this->_params['username'], + $this->_params['password'], + $domain_socket, + 0, + null, + null, + false, + true, + $this->_params['usetls']); + + $res = $this->_sieve->getError(); + if (is_a($res, 'PEAR_Error')) { + unset($this->_sieve); + return $res; + } else { + return true; + } + } + + /** + * Sets a script running on the backend. + * + * @param string $script The sieve script. + * + * @return mixed True on success. + * Returns PEAR_Error on error. + */ + public function setScriptActive($script) + { + $res = $this->_connect(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + + $res = $this->_sieve->haveSpace($this->_params['scriptname'], strlen($script)); + if (is_a($res, 'PEAR_ERROR')) { + return $res; + } + + return $this->_sieve->installScript($this->_params['scriptname'], $script, true); + } + + /** + * Returns the content of the currently active script. + * + * @return string The complete ruleset of the specified user. + */ + public function getScript() + { + $res = $this->_connect(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + return $this->_sieve->getScript($this->_sieve->getActive()); + } + + /** + * Used to figure out which Sieve server the script will be run + * on, and then open a GSSAPI authenticated socket to said server. + * + * @param string $username The username. + * @param string $password The password. + * @param string $hostspec The hostspec. + * + * @return TODO + */ + public function sivtestSocket($username, $password, $hostspec) + { + $command = ''; + $error_return = ''; + + if (strtolower($this->_params['logintype']) == 'gssapi' + && isset($_SERVER['KRB5CCNAME'])) { + $command .= 'KRB5CCNAME=' . $_SERVER['KRB5CCNAME']; + } + + if (substr(PHP_VERSION, 0, 1) == '5') { + $domain_socket = 'unix://' . $this->_params['socket']; + } else { + $domain_socket = $this->_params['socket']; + } + + $command .= ' ' . $this->_params['command'] + . ' -m ' . $this->_params['logintype'] + . ' -u ' . $username + . ' -a ' . $username + . ' -w ' . $password + . ' -p ' . $this->_params['port'] + . ' -X ' . $this->_params['socket'] + . ' ' . $hostspec; + + $conn_attempts = 0; + while ($conn_attempts++ < 4) { + $attempts = 0; + if (!file_exists($this->_params['socket'])) { + exec($command . ' > /dev/null 2>&1'); + sleep(1); + while (!file_exists($this->_params['socket'])) { + usleep(200000); + if ($attempts++ > 5) { + $error_return = ': No socket after 10 seconds of trying!'; + continue 2; + } + } + } + $socket = new Net_Socket(); + $error = $socket->connect($domain_socket, 0, true, 30); + if (!is_a($error, 'PEAR_Error')) { + break; + } + + // We failed, break this connection. + unlink($this->_params['socket']); + } + + if (!empty($error_return)) { + return PEAR::raiseError(_($error_return)); + } + + $status = $socket->getStatus(); + if (is_a($status, 'PEAR_Error') || $status['eof']) { + return PEAR::raiseError(_('Failed to write to socket: (connection lost!)')); + } + + $error = $socket->writeLine("CAPABILITY"); + if (is_a($error, 'PEAR_Error')) { + return PEAR::raiseError(_('Failed to write to socket: ' . $error->getMessage())); + } + + $result = $socket->readLine(); + if (is_a($result, 'PEAR_Error')) { + return PEAR::raiseError(_('Failed to read from socket: ' . $error->getMessage())); + } + + if (preg_match('|^bye \(referral "(sieve://)?([^"]+)|i', + $result, $matches)) { + $socket->disconnect(); + + $this->sivtestSocket($username, $password, $matches[2]); + } else { + $socket->disconnect(); + exec($command . ' > /dev/null 2>&1'); + sleep(1); + } + } + +} diff --git a/ingo/lib/Driver/timsieved.php b/ingo/lib/Driver/timsieved.php new file mode 100644 index 000000000..afc9ffa24 --- /dev/null +++ b/ingo/lib/Driver/timsieved.php @@ -0,0 +1,124 @@ + + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Driver_timsieved extends Ingo_Driver +{ + /** + * The Net_Sieve object. + * + * @var Net_Sieve + */ + protected $_sieve; + + /** + * Constructor. + */ + public function __construct($params = array()) + { + $this->_support_shares = true; + + $default_params = array( + 'hostspec' => 'localhost', + 'logintype' => 'PLAIN', + 'port' => 2000, + 'scriptname' => 'ingo', + 'admin' => '', + 'usetls' => true + ); + + parent::__construct(array_merge($default_params, $params)); + } + + /** + * Connect to the sieve server. + * + * @return mixed True on success, PEAR_Error on false. + */ + public function _connect() + { + if (!empty($this->_sieve)) { + return true; + } + + if (empty($this->_params['admin'])) { + $auth = $this->_params['username']; + } else { + $auth = $this->_params['admin']; + } + $this->_sieve = new Net_Sieve($auth, + $this->_params['password'], + $this->_params['hostspec'], + $this->_params['port'], + $this->_params['logintype'], + Ingo::getUser(false), + false, + false, + $this->_params['usetls']); + + $res = $this->_sieve->getError(); + if (is_a($res, 'PEAR_Error')) { + unset($this->_sieve); + return $res; + } else { + return true; + } + } + + /** + * Sets a script running on the backend. + * + * @param string $script The sieve script. + * + * @return mixed True on success, PEAR_Error on error. + */ + public function setScriptActive($script) + { + $res = $this->_connect(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + + if (!strlen($script)) { + return $this->_sieve->setActive(''); + } + + $res = $this->_sieve->haveSpace($this->_params['scriptname'], + strlen($script)); + if (is_a($res, 'PEAR_ERROR')) { + return $res; + } + + return $this->_sieve->installScript($this->_params['scriptname'], + $script, true); + } + + /** + * Returns the content of the currently active script. + * + * @return string The complete ruleset of the specified user. + */ + public function getScript() + { + $res = $this->_connect(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + $active = $this->_sieve->getActive(); + if (empty($active)) { + return ''; + } + return $this->_sieve->getScript($active); + } + +} diff --git a/ingo/lib/Driver/vfs.php b/ingo/lib/Driver/vfs.php new file mode 100644 index 000000000..257a97c60 --- /dev/null +++ b/ingo/lib/Driver/vfs.php @@ -0,0 +1,143 @@ + + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Driver_vfs extends Ingo_Driver +{ + /** + * Constructs a new VFS-based storage driver. + * + * @param array $params A hash containing driver parameters. + */ + public function __construct($params = array()) + { + $this->_support_shares = true; + + $default_params = array( + 'hostspec' => 'localhost', + 'port' => 21, + 'filename' => '.ingo_filter', + 'vfstype' => 'ftp', + 'vfs_path' => '', + 'vfs_forward_path' => '', + ); + + parent::__construct(array_merge($default_params, $params)); + } + + /** + * Sets a script running on the backend. + * + * @param string $script The filter script + * + * @return mixed True on success, or PEAR_Error on failure. + */ + public function setScriptActive($script) + { + $result = $this->_connect(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + if (empty($script)) { + $result = $this->_vfs->deleteFile($this->_params['vfs_path'], $this->_params['filename']); + } else { + $result = $this->_vfs->writeData($this->_params['vfs_path'], $this->_params['filename'], $script, true); + } + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + if (isset($this->_params['file_perms']) && !empty($script)) { + $result = $this->_vfs->changePermissions($this->_params['vfs_path'], $this->_params['filename'], $this->_params['file_perms']); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + // Get the backend; necessary if a .forward is needed for + // procmail. + $backend = Ingo::getBackend(); + if ($backend['script'] == 'procmail' && isset($backend['params']['forward_file']) && isset($backend['params']['forward_string'])) { + if (empty($script)) { + $result = $this->_vfs->deleteFile($this->_params['vfs_forward_path'], $backend['params']['forward_file']); + } else { + $result = $this->_vfs->writeData($this->_params['vfs_forward_path'], $backend['params']['forward_file'], $backend['params']['forward_string'], true); + } + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + if (isset($this->_params['file_perms']) && !empty($script)) { + $result = $this->_vfs->changePermissions($this->_params['vfs_forward_path'], $backend['params']['forward_file'], $this->_params['file_perms']); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + } + + return true; + } + + /** + * Returns the content of the currently active script. + * + * @return string The complete ruleset of the specified user. + */ + public function getScript() + { + $result = $this->_connect(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + return $this->_vfs->read('', $this->_params['vfs_path'] . '/' . $this->_params['filename']); + } + + /** + * Connect to the VFS server. + * + * @return boolean True on success, PEAR_Error on false. + */ + protected function _connect() + { + /* Do variable substitution. */ + if (!empty($this->_params['vfs_path'])) { + $user = Ingo::getUser(); + $domain = Ingo::getDomain(); + if ($_SESSION['ingo']['backend']['hordeauth'] !== 'full') { + $pos = strpos($user, '@'); + if ($pos !== false) { + $domain = substr($user, $pos + 1); + $user = substr($user, 0, $pos); + } + } + $this->_params['vfs_path'] = str_replace( + array('%u', '%d', '%U'), + array($user, $domain, $this->_params['username']), + $this->_params['vfs_path']); + } + + if (!empty($this->_vfs)) { + return true; + } + + $this->_vfs = &VFS::singleton($this->_params['vfstype'], $this->_params); + if (is_a($this->_vfs, 'PEAR_Error')) { + $error = $this->_vfs; + $this->_vfs = null; + return $error; + } else { + return true; + } + } + +} diff --git a/ingo/lib/Ingo.php b/ingo/lib/Ingo.php new file mode 100644 index 000000000..46a7ca2cd --- /dev/null +++ b/ingo/lib/Ingo.php @@ -0,0 +1,424 @@ + + * @author Jan Schneider + * @package Ingo + */ +class Ingo +{ + /** + * String that can't be a valid folder name used to mark blacklisted email + * as deleted. + */ + const BLACKLIST_MARKER = '++DELETE++'; + + /** + * Define the key to use to indicate a user-defined header is requested. + */ + const USER_HEADER = '++USER_HEADER++'; + + /** + * Generates a folder widget. + * If an application is available that provides a folderlist method + * then a <select> input is created else a simple text field + * is returned. + * + * @param string $value The current value for the field. + * @param string $form The form name for the newFolderName() call. + * @param string $tagname The label for the select tag. + * @param string $onchange Javascript code to execute onchange. + * + * @return string The HTML to render the field. + */ + static public function flistSelect($value = null, $form = null, + $tagname = 'actionvalue', + $onchange = null) + { + global $conf, $registry; + + if (!empty($conf['rules']['usefolderapi']) && + $registry->hasMethod('mail/folderlist')) { + $mailboxes = $registry->call('mail/folderlist'); + if (!is_a($mailboxes, 'PEAR_Error')) { + $createfolder = $registry->hasMethod('mail/createFolder'); + + $text = ''; + return $text; + } + } + + return ''; + } + + /** + * Creates a new IMAP folder via an api call. + * + * @param string $folder The name of the folder to create. + * + * @return boolean True on success, false if not created, PEAR_Error on + * failure. + */ + static public function createFolder($folder) + { + return $GLOBALS['registry']->hasMethod('mail/createFolder') + ? $GLOBALS['registry']->call('mail/createFolder', array('folder' => String::convertCharset($folder, NLS::getCharset(), 'UTF7-IMAP'))); + : false; + } + + /** + * Returns the user whose rules are currently being edited. + * + * @param boolean $full Always return the full user name with realm? + * + * @return string The current user. + */ + static public function getUser($full = true) + { + if (empty($GLOBALS['ingo_shares'])) { + $user = ($full || + (isset($_SESSION['ingo']['backend']['hordeauth']) && + $_SESSION['ingo']['backend']['hordeauth'] === 'full')) ? + Auth::getAuth() : + Auth::getBareAuth(); + } else { + list(, $user) = explode(':', $_SESSION['ingo']['current_share'], 2); + } + + return $user; + } + + /** + * Returns the domain name, if any of the user whose rules are currently + * being edited. + * + * @return string The current user's domain name. + */ + static public function getDomain() + { + $user = self::getUser(true); + $pos = strpos($user, '@'); + + return ($pos !== false) + ? substr($user, $pos + 1) + : false; + } + + /** + * Connects to the backend and uploads the script and sets it active. + * + * @param string $script The script to set active. + * @param boolean $deactivate If true, notification will identify the + * script as deactivated instead of activated. + * + * @return boolean True on success, false on failure. + */ + static public function activateScript($script, $deactivate = false) + { + global $notification; + + $driver = self::getDriver(); + $res = $driver->setScriptActive($script); + if (is_a($res, 'PEAR_Error')) { + $msg = ($deactivate) + ? _("There was an error deactivating the script.") + : _("There was an error activating the script."); + $notification->push($msg . ' ' . _("The driver said: ") . $res->getMessage(), 'horde.error'); + return false; + } elseif ($res === true) { + $msg = ($deactivate) + ? _("Script successfully deactivated.") + : _("Script successfully activated."); + $notification->push($msg, 'horde.success'); + return true; + } + + return false; + } + + /** + * Connects to the backend and returns the currently active script. + * + * @return string The currently active script. + */ + static public function getScript() + { + $driver = self::getDriver(); + return $driver->getScript(); + } + + /** + * Does all the work in updating the script on the server. + */ + static public function updateScript() + { + global $notification; + + if ($_SESSION['ingo']['script_generate']) { + $ingo_script = self::loadIngoScript(); + if (!$ingo_script) { + $notification->push(_("Script not updated."), 'horde.error'); + } else { + /* Generate and activate the script. */ + $script = $ingo_script->generate(); + self::activateScript($script); + } + } + } + + /** + * Determine the backend to use. + * + * This decision is based on the global 'SERVER_NAME' and 'HTTP_HOST' + * server variables and the contents of the 'preferred' either field + * in the backend's definition. The 'preferred' field may take a + * single value or an array of multiple values. + * + * @return array The backend entry. + * Calls Horde::fatal() on error. + */ + static public function getBackend() + { + include INGO_BASE . '/config/backends.php'; + if (!isset($backends) || !is_array($backends)) { + Horde::fatal(PEAR::raiseError(_("No backends configured in backends.php")), __FILE__, __LINE__); + } + + $backend = null; + foreach ($backends as $name => $temp) { + if (!isset($backend)) { + $backend = $name; + } elseif (!empty($temp['preferred'])) { + if (is_array($temp['preferred'])) { + foreach ($temp['preferred'] as $val) { + if (($val == $_SERVER['SERVER_NAME']) || + ($val == $_SERVER['HTTP_HOST'])) { + $backend = $name; + } + } + } elseif (($temp['preferred'] == $_SERVER['SERVER_NAME']) || + ($temp['preferred'] == $_SERVER['HTTP_HOST'])) { + $backend = $name; + } + } + } + + /* Check for valid backend configuration. */ + if (!isset($backend)) { + Horde::fatal(PEAR::raiseError(_("No backend configured for this host")), __FILE__, __LINE__); + } + + $backends[$backend]['id'] = $name; + $backend = $backends[$backend]; + + if (empty($backend['script'])) { + Horde::fatal(PEAR::raiseError(sprintf(_("No \"%s\" element found in backend configuration."), 'script')), __FILE__, __LINE__); + } elseif (empty($backend['driver'])) { + Horde::fatal(PEAR::raiseError(sprintf(_("No \"%s\" element found in backend configuration."), 'driver')), __FILE__, __LINE__); + } + + /* Make sure the 'params' entry exists. */ + if (!isset($backend['params'])) { + $backend['params'] = array(); + } + + return $backend; + } + + /** + * Loads a Ingo_Script:: backend and checks for errors. + * + * @return Ingo_Script Script object on success, PEAR_Error on failure. + */ + static public function loadIngoScript() + { + $ingo_script = Ingo_Script::factory($_SESSION['ingo']['backend']['script'], + isset($_SESSION['ingo']['backend']['scriptparams']) ? $_SESSION['ingo']['backend']['scriptparams'] : array()); + if (is_a($ingo_script, 'PEAR_Error')) { + Horde::fatal($ingo_script, __FILE__, __LINE__); + } + + return $ingo_script; + } + + /** + * Returns an instance of the configured driver. + * + * @return Ingo_Driver The configured driver. + */ + static public function getDriver() + { + $params = $_SESSION['ingo']['backend']['params']; + + // Set authentication parameters. + if (!empty($_SESSION['ingo']['backend']['hordeauth'])) { + $params['username'] = ($_SESSION['ingo']['backend']['hordeauth'] === 'full') + ? Auth::getAuth() : Auth::getBareAuth(); + $params['password'] = Auth::getCredential('password'); + } elseif (isset($_SESSION['ingo']['backend']['params']['username']) && + isset($_SESSION['ingo']['backend']['params']['password'])) { + $params['username'] = $_SESSION['ingo']['backend']['params']['username']; + $params['password'] = $_SESSION['ingo']['backend']['params']['password']; + } else { + $params['username'] = Auth::getBareAuth(); + $params['password'] = Auth::getCredential('password'); + } + + return Ingo_Driver::factory($_SESSION['ingo']['backend']['driver'], $params); + } + + /** + * Returns all rulesets a user has access to, according to several + * parameters/permission levels. + * + * @param boolean $owneronly Only return rulesets that this user owns? + * Defaults to false. + * @param integer $permission The permission to filter rulesets by. + * + * @return array The ruleset list. + */ + static public function listRulesets($owneronly = false, + $permission = PERMS_SHOW) + { + $rulesets = $GLOBALS['ingo_shares']->listShares(Auth::getAuth(), $permission, $owneronly ? Auth::getAuth() : null); + if (is_a($rulesets, 'PEAR_Error')) { + Horde::logMessage($rulesets, __FILE__, __LINE__, PEAR_LOG_ERR); + return array(); + } + + return $rulesets; + } + + /** + * Returns the specified permission for the current user. + * + * @param string $permission A permission, either 'allow_rules' or + * 'max_rules'. + * + * @return mixed The value of the specified permission. + */ + static public function hasPermission($permission, $mask = null) + { + if ($permission == 'shares') { + if (!isset($GLOBALS['ingo_shares'])) { + return true; + } + static $all_perms; + if (!isset($all_perms)) { + $all_perms = $GLOBALS['ingo_shares']->getPermissions($_SESSION['ingo']['current_share'], Auth::getAuth()); + } + return $all_perms & $mask; + } + + global $perms; + + if (!$perms->exists('ingo:' . $permission)) { + return true; + } + + $allowed = $perms->getPermissions('ingo:' . $permission); + if (is_array($allowed)) { + switch ($permission) { + case 'allow_rules': + $allowed = (bool)count(array_filter($allowed)); + break; + + case 'max_rules': + $allowed = max($allowed); + break; + } + } + + return $allowed; + } + + /** + * Returns whether an address is empty or only contains a "@". + * Helper function for array_filter(). + * + * @param string $address An email address to test. + * + * @return boolean True if the address is not empty. + */ + static protected function _filterEmptyAddress($address) + { + $address = trim($address); + return !empty($address) && $address != '@'; + } + + /** + * Build Ingo's list of menu items. + */ + static public function getMenu($returnType = 'object') + { + require_once 'Horde/Menu.php'; + + $menu = new Menu(); + $menu->add(Horde::applicationUrl('filters.php'), _("Filter _Rules"), 'ingo.png', null, null, null, basename($_SERVER['PHP_SELF']) == 'index.php' ? 'current' : null); + if (!is_a($whitelist_url = $GLOBALS['registry']->link('mail/showWhitelist'), 'PEAR_Error')) { + $menu->add(Horde::url($whitelist_url), _("_Whitelist"), 'whitelist.png'); + } + if (!is_a($blacklist_url = $GLOBALS['registry']->link('mail/showBlacklist'), 'PEAR_Error')) { + $menu->add(Horde::url($blacklist_url), _("_Blacklist"), 'blacklist.png'); + } + if (in_array(INGO_STORAGE_ACTION_VACATION, $_SESSION['ingo']['script_categories'])) { + $menu->add(Horde::applicationUrl('vacation.php'), _("_Vacation"), 'vacation.png'); + } + if (in_array(INGO_STORAGE_ACTION_FORWARD, $_SESSION['ingo']['script_categories'])) { + $menu->add(Horde::applicationUrl('forward.php'), _("_Forward"), 'forward.png'); + } + if (in_array(INGO_STORAGE_ACTION_SPAM, $_SESSION['ingo']['script_categories'])) { + $menu->add(Horde::applicationUrl('spam.php'), _("S_pam"), 'spam.png'); + } + if ($_SESSION['ingo']['script_generate'] && + (!$GLOBALS['prefs']->isLocked('auto_update') || + !$GLOBALS['prefs']->getValue('auto_update'))) { + $menu->add(Horde::applicationUrl('script.php'), _("_Script"), 'script.png'); + } + if (!empty($GLOBALS['ingo_shares']) && empty($GLOBALS['conf']['share']['no_sharing'])) { + $menu->add('#', _("_Permissions"), 'perms.png', $GLOBALS['registry']->getImageDir('horde'), '', 'popup(\'' . Util::addParameter(Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/shares/edit.php', true), array('app' => 'ingo', 'share' => htmlspecialchars($_SESSION['ingo']['backend']['id'] . ':' . Auth::getAuth())), null, false) . '\');return false;'); + } + + if ($returnType == 'object') { + return $menu; + } else { + return $menu->render(); + } + } + +} diff --git a/ingo/lib/Script.php b/ingo/lib/Script.php new file mode 100644 index 000000000..9966d4ad9 --- /dev/null +++ b/ingo/lib/Script.php @@ -0,0 +1,339 @@ + + * @package Ingo + */ +class Ingo_Script +{ + /** + * Only filter unseen messages. + */ + const INGO_SCRIPT_FILTER_UNSEEN = 1; + + /** + * Only filter seen messages. + */ + const INGO_SCRIPT_FILTER_SEEN = 2; + + /** + * The script class' additional parameters. + * + * @var array + */ + protected $_params = array(); + + /** + * The list of actions allowed (implemented) for this driver. + * This SHOULD be defined in each subclass. + * + * @var array + */ + protected $_actions = array(); + + /** + * The categories of filtering allowed. + * This SHOULD be defined in each subclass. + * + * @var array + */ + protected $_categories = array(); + + /** + * The list of tests allowed (implemented) for this driver. + * This SHOULD be defined in each subclass. + * + * @var array + */ + protected $_tests = array(); + + /** + * The types of tests allowed (implemented) for this driver. + * This SHOULD be defined in each subclass. + * + * @var array + */ + protected $_types = array(); + + /** + * A list of any special types that this driver supports. + * + * @var array + */ + protected $_special_types = array(); + + /** + * Can tests be case sensitive? + * + * @var boolean + */ + protected $_casesensitive = false; + + /** + * Does the driver support setting IMAP flags? + * + * @var boolean + */ + protected $_supportIMAPFlags = false; + + /** + * Does the driver support the stop-script option? + * + * @var boolean + */ + protected $_supportStopScript = false; + + /** + * Can this driver perform on demand filtering? + * + * @var boolean + */ + protected $_ondemand = false; + + /** + * Does the driver require a script file to be generated? + * + * @var boolean + */ + protected $_scriptfile = false; + + /** + * Attempts to return a concrete Ingo_Script instance based on $script. + * + * @param string $script The type of Ingo_Script subclass to return. + * @param array $params Hash containing additional paramters to be passed + * to the subclass' constructor. + * + * @return Ingo_Script The newly created concrete Ingo_Script instance, or + * false on error. + */ + static public function factory($script, $params = array()) + { + $script = basename($script); + $class = 'Ingo_Script_' . $script; + + return class_exists($class) + ? new $class($params) + : PEAR::raiseError(sprintf(_("Unable to load the definition of %s."), $class)); + } + + /** + * Constructor. + * + * @param array $params A hash containing parameters needed. + */ + public function __construct($params = array()) + { + global $registry; + + $this->_params = $params; + + /* Determine if ingo should handle the blacklist. */ + $key = array_search(Ingo_Storage::ACTION_BLACKLIST, $this->_categories); + if ($key !== false && ($registry->hasMethod('mail/blacklistFrom') != 'ingo')) { + unset($this->_categories[$key]); + } + + /* Determine if ingo should handle the whitelist. */ + $key = array_search(Ingo_Storage::ACTION_WHITELIST, $this->_categories); + if ($key !== false && ($registry->hasMethod('mail/whitelistFrom') != 'ingo')) { + unset($this->_categories[$key]); + } + } + + /** + * Returns a regular expression that should catch mails coming from most + * daemons, mailing list, newsletters, and other bulk. + * + * This is the expression used for procmail's FROM_DAEMON, including all + * mailinglist headers. + * + * @return string A regular expression. + */ + public function excludeRegexp() + { + return '(^(Mailing-List:|List-(Id|Help|Unsubscribe|Subscribe|Owner|Post|Archive):|Precedence:.*(junk|bulk|list)|To: Multiple recipients of|(((Resent-)?(From|Sender)|X-Envelope-From):|>?From)([^>]*[^(.%@a-z0-9])?(Post(ma?(st(e?r)?|n)|office)|(send)?Mail(er)?|daemon|m(mdf|ajordomo)|n?uucp|LIST(SERV|proc)|NETSERV|o(wner|ps)|r(e(quest|sponse)|oot)|b(ounce|bs\.smtp)|echo|mirror|s(erv(ices?|er)|mtp(error)?|ystem)|A(dmin(istrator)?|MMGR|utoanswer))(([^).!:a-z0-9][-_a-z0-9]*)?[%@>\t ][^<)]*(\(.*\).*)?)?$([^>]|$)))'; + } + + /** + * Returns the available actions for this driver. + * + * @return array The list of available actions. + */ + public function availableActions() + { + return $this->_actions; + } + + /** + * Returns the available categories for this driver. + * + * @return array The list of categories. + */ + public function availableCategories() + { + return $this->_categories; + } + + /** + * Returns the available tests for this driver. + * + * @return array The list of tests actions. + */ + public function availableTests() + { + return $this->_tests; + } + + /** + * Returns the available test types for this driver. + * + * @return array The list of test types. + */ + public function availableTypes() + { + return $this->_types; + } + + /** + * Returns any test types that are special for this driver. + * + * @return array The list of special types + */ + public function specialTypes() + { + return $this->_special_types; + } + + /** + * Returns if this driver allows case sensitive searches. + * + * @return boolean Does this driver allow case sensitive searches? + */ + public function caseSensitive() + { + return $this->_casesensitive; + } + + /** + * Returns if this driver allows IMAP flags to be set. + * + * @return boolean Does this driver allow IMAP flags to be set? + */ + public function imapFlags() + { + return $this->_supportIMAPFlags; + } + + /** + * Returns if this driver supports the stop-script option. + * + * @return boolean Does this driver support the stop-script option? + */ + public function stopScript() + { + return $this->_supportStopScript; + } + + /** + * Returns a script previously generated with generate(). + * + * @abstract + * + * @return string The script. + */ + public function toCode() + { + return ''; + } + + /** + * Can this driver generate a script file? + * + * @return boolean True if generate() is available, false if not. + */ + public function generateAvailable() + { + return $this->_scriptfile; + } + + /** + * Generates the script to do the filtering specified in + * the rules. + * + * @abstract + * + * @return string The script. + */ + public function generate() + { + return ''; + } + + /** + * Can this driver perform on demand filtering? + * + * @return boolean True if perform() is available, false if not. + */ + public function performAvailable() + { + return $this->_ondemand; + } + + /** + * Perform the filtering specified in the rules. + * + * @abstract + * + * @param array $params The parameter array. + * + * @return boolean True if filtering performed, false if not. + */ + public function perform($params = array()) + { + return false; + } + + /** + * Is the apply() function available? + * + * @return boolean True if apply() is available, false if not. + */ + public function canApply() + { + return $this->performAvailable(); + } + + /** + * Apply the filters now. + * This is essentially a wrapper around perform() that allows that + * function to be called from within Ingo ensuring that all necessary + * parameters are set. + * + * @abstract + * + * @return boolean See perform(). + */ + public function apply() + { + return $this->perform(); + } + + /** + * Is this a valid rule? + * + * @param integer $type The rule type. + * + * @return boolean Whether the rule is valid or not for this driver. + */ + protected function _validRule($type) + { + return (!empty($type) && in_array($type, array_merge($this->_categories, $this->_actions))); + } + +} diff --git a/ingo/lib/Script/imap.php b/ingo/lib/Script/imap.php new file mode 100644 index 000000000..ad57e37e3 --- /dev/null +++ b/ingo/lib/Script/imap.php @@ -0,0 +1,463 @@ + + * @package Ingo + */ +class Ingo_Script_imap extends Ingo_Script { + + /** + * The list of actions allowed (implemented) for this driver. + * + * @var array + */ + var $_actions = array( + Ingo_Storage::ACTION_KEEP, + Ingo_Storage::ACTION_MOVE, + Ingo_Storage::ACTION_DISCARD, + Ingo_Storage::ACTION_MOVEKEEP + ); + + /** + * The categories of filtering allowed. + * + * @var array + */ + var $_categories = array( + Ingo_Storage::ACTION_BLACKLIST, + Ingo_Storage::ACTION_WHITELIST + ); + + /** + * The list of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_tests = array( + 'contains', 'not contain' + ); + + /** + * The types of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_types = array( + Ingo_Storage::TYPE_HEADER, + Ingo_Storage::TYPE_SIZE, + Ingo_Storage::TYPE_BODY + ); + + /** + * Does the driver support setting IMAP flags? + * + * @var boolean + */ + var $_supportIMAPFlags = true; + + /** + * Does the driver support the stop-script option? + * + * @var boolean + */ + var $_supportStopScript = true; + + /** + * This driver can perform on demand filtering (in fact, that is all + * it can do). + * + * @var boolean + */ + var $_ondemand = true; + + /** + * The API to use for IMAP functions. + * + * @var Ingo_Script_imap_api + */ + var $_api; + + /** + * Perform the filtering specified in the rules. + * + * @param array $params The parameter array. It MUST contain: + *
+     * 'imap'     --  An open IMAP stream.
+     * 'mailbox'  --  The name of the mailbox to filter.
+     * 
+ * + * @return boolean True if filtering performed, false if not. + */ + function perform($params) + { + global $ingo_storage, $notification, $prefs; + + if (empty($params['api'])) { + $this->_api = Ingo_Script_imap_api::factory('live', $params); + } else { + $this->_api = &$params['api']; + } + + /* Indices that will be ignored by subsequent rules. */ + $ignore_ids = array(); + + /* Only do filtering if: + 1. We have not done filtering before -or- + 2. The mailbox has changed -or- + 3. The rules have changed. */ + $cache = $this->_api->getCache(); + if (($cache !== false) && ($cache == $_SESSION['ingo']['change'])) { + return true; + } + + /* Grab the rules list. */ + $filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); + + /* Should we filter only [un]seen messages? */ + $seen_flag = $prefs->getValue('filter_seen'); + + /* Should we use detailed notification messages? */ + $detailmsg = $prefs->getValue('show_filter_msg'); + + /* Parse through the rules, one-by-one. */ + foreach ($filters->getFilterlist() as $rule) { + /* Check to make sure this is a valid rule and that the rule is + not disabled. */ + if (!$this->_validRule($rule['action']) || + !empty($rule['disable'])) { + continue; + } + + $search_array = array(); + + switch ($rule['action']) { + case Ingo_Storage::ACTION_BLACKLIST: + case Ingo_Storage::ACTION_WHITELIST: + $bl_folder = null; + + if ($rule['action'] == Ingo_Storage::ACTION_BLACKLIST) { + $blacklist = &$ingo_storage->retrieve(Ingo_Storage::ACTION_BLACKLIST); + $addr = $blacklist->getBlacklist(); + $bl_folder = $blacklist->getBlacklistFolder(); + } else { + $whitelist = &$ingo_storage->retrieve(Ingo_Storage::ACTION_WHITELIST); + $addr = $whitelist->getWhitelist(); + } + + /* If list is empty, move on. */ + if (empty($addr)) { + continue; + } + + $query = new Ingo_IMAP_Search_Query(); + foreach ($addr as $val) { + $ob = new Ingo_IMAP_Search_Query(); + $ob->deleted(false); + if ($seen_flag == Ingo_Script::FILTER_UNSEEN) { + $ob->seen(false); + } elseif ($seen_flag == Ingo_Script::FILTER_SEEN) { + $ob->seen(true); + } + $ob->header('from', $val); + $search_array[] = $ob; + } + $query->imapOr($search_array); + $indices = $this->_api->search($query); + + /* Remove any indices that got in there by way of partial + * address match. */ + $sequence = implode(',', $indices); + $msgs = $this->_api->fetchMessageOverviews($sequence); + foreach ($msgs as $msg) { + $from_addr = MIME::bareAddress($msg->from); + $found = false; + foreach ($addr as $val) { + if (strtolower($from_addr) == strtolower($val)) { + $found = true; + } + } + if (!$found) { + $indices = array_diff($indices, array($msg->uid)); + } + } + + if ($rule['action'] == Ingo_Storage::ACTION_BLACKLIST) { + $indices = array_diff($indices, $ignore_ids); + if (!empty($indices)) { + $sequence = implode(',', $indices); + if (!empty($bl_folder)) { + $this->_api->moveMessages($sequence, $bl_folder); + } else { + $this->_api->deleteMessages($sequence); + } + $this->_api->expunge($indices); + $notification->push(sprintf(_("Filter activity: %s message(s) that matched the blacklist were deleted."), count($indices)), 'horde.message'); + } + } else { + $ignore_ids = $indices; + } + break; + + case Ingo_Storage::ACTION_KEEP: + case Ingo_Storage::ACTION_MOVE: + case Ingo_Storage::ACTION_DISCARD: + $query = new Ingo_IMAP_Search_Query(); + foreach ($rule['conditions'] as $val) { + $ob = new Ingo_IMAP_Search_Query(); + $ob->deleted(false); + if ($seen_flag == Ingo_Script::FILTER_UNSEEN) { + $ob->seen(false); + } elseif ($seen_flag == Ingo_Script::FILTER_SEEN) { + $ob->seen(true); + } + if (!empty($val['type']) && + ($val['type'] == Ingo_Storage::TYPE_SIZE)) { + if ($val['match'] == 'greater than') { + $operator = '>'; + } elseif ($val['match'] == 'less than') { + $operator = '<'; + } + $ob->size($val['value'], $operator); + } elseif (!empty($val['type']) && + ($val['type'] == Ingo_Storage::TYPE_BODY)) { + if ($val['match'] == 'contains') { + $ob->body($val['value'], false); + } elseif ($val['match'] == 'not contain') { + $ob->body($val['value'], true); + } + } else { + if ($val['match'] == 'contains') { + $ob->header($val['field'], $val['value'], false); + } elseif ($val['match'] == 'not contain') { + $ob->header($val['field'], $val['value'], true); + } + } + $search_array[] = $ob; + } + + if ($rule['combine'] == Ingo_Storage::COMBINE_ALL) { + $query->imapAnd($search_array); + } else { + $query->imapOr($search_array); + } + + $indices = $this->_api->search($query); + + if (($indices = array_diff($indices, $ignore_ids))) { + if ($rule['stop']) { + /* If the stop action is set, add these + * indices to the list of ids that will be + * ignored by subsequent rules. */ + $ignore_ids = array_unique($indices + $ignore_ids); + } + + $sequence = implode(',', $indices); + + /* Set the flags. */ + if (!empty($rule['flags']) && + ($rule['action'] != Ingo_Storage::ACTION_DISCARD)) { + $flags = array(); + if ($rule['flags'] & Ingo_Storage::FLAG_ANSWERED) { + $flags[] = '\\Answered'; + } + if ($rule['flags'] & Ingo_Storage::FLAG_DELETED) { + $flags[] = '\\Deleted'; + } + if ($rule['flags'] & Ingo_Storage::FLAG_FLAGGED) { + $flags[] = '\\Flagged'; + } + if ($rule['flags'] & Ingo_Storage::FLAG_SEEN) { + $flags[] = '\\Seen'; + } + $this->_api->setMessageFlags($sequence, + implode(' ', $flags)); + } + + if ($rule['action'] == Ingo_Storage::ACTION_KEEP) { + /* Add these indices to the ignore list. */ + $ignore_ids = array_unique($indices + $ignore_ids); + } elseif ($rule['action'] == Ingo_Storage::ACTION_MOVE) { + /* We need to grab the overview first. */ + if ($detailmsg) { + $overview = $this->_api->fetchMessageOverviews($sequence); + } + + /* Move the messages to the requested mailbox. */ + $this->_api->moveMessages($sequence, + $rule['action-value']); + $this->_api->expunge($indices); + + /* Display notification message(s). */ + if ($detailmsg) { + foreach ($overview as $msg) { + $notification->push( + sprintf(_("Filter activity: The message \"%s\" from \"%s\" has been moved to the folder \"%s\"."), + isset($msg->subject) ? MIME::decode($msg->subject, NLS::getCharset()) : _("[No Subject]"), + isset($msg->from) ? MIME::decode($msg->from, NLS::getCharset()) : _("[No Sender]"), + String::convertCharset($rule['action-value'], 'UTF7-IMAP', NLS::getCharset())), + 'horde.message'); + } + } else { + $notification->push(sprintf(_("Filter activity: %s message(s) have been moved to the folder \"%s\"."), + count($indices), + String::convertCharset($rule['action-value'], 'UTF7-IMAP', NLS::getCharset())), 'horde.message'); + } + } elseif ($rule['action'] == Ingo_Storage::ACTION_DISCARD) { + /* We need to grab the overview first. */ + if ($detailmsg) { + $overview = $this->_api->fetchMessageOverviews($sequence); + } + + /* Delete the messages now. */ + $this->_api->deleteMessages($sequence); + $this->_api->expunge($indices); + + /* Display notification message(s). */ + if ($detailmsg) { + foreach ($overview as $msg) { + $notification->push( + sprintf(_("Filter activity: The message \"%s\" from \"%s\" has been deleted."), + isset($msg->subject) ? MIME::decode($msg->subject, NLS::getCharset()) : _("[No Subject]"), + isset($msg->from) ? MIME::decode($msg->from, NLS::getCharset()) : _("[No Sender]")), + 'horde.message'); + } + } else { + $notification->push(sprintf(_("Filter activity: %s message(s) have been deleted."), count($indices)), 'horde.message'); + } + } elseif ($rule['action'] == Ingo_Storage::ACTION_MOVEKEEP) { + /* Copy the messages to the requested mailbox. */ + $this->_api->copyMessages($sequence, + $rule['action-value']); + + /* Display notification message(s). */ + if ($detailmsg) { + $overview = $this->_api->fetchMessageOverviews($sequence); + foreach ($overview as $msg) { + $notification->push( + sprintf(_("Filter activity: The message \"%s\" from \"%s\" has been copied to the folder \"%s\"."), + isset($msg->subject) ? MIME::decode($msg->subject, NLS::getCharset()) : _("[No Subject]"), + isset($msg->from) ? MIME::decode($msg->from, NLS::getCharset()) : _("[No Sender]"), + String::convertCharset($rule['action-value'], 'UTF7-IMAP', NLS::getCharset())), + 'horde.message'); + } + } else { + $notification->push(sprintf(_("Filter activity: %s message(s) have been copied to the folder \"%s\"."), count($indices), String::convertCharset($rule['action-value'], 'UTF7-IMAP', NLS::getCharset())), 'horde.message'); + } + } + } + break; + } + } + + /* Set cache flag. */ + $this->_api->storeCache($_SESSION['ingo']['change']); + return true; + } + + /** + * Is the apply() function available? + * The 'mail/getStream' API function must be available. + * + * @return boolean True if apply() is available, false if not. + */ + function canApply() + { + global $registry; + + return ($this->performAvailable() && $registry->hasMethod('mail/getStream')); + } + + /** + * Apply the filters now. + * + * @return boolean See perform(). + */ + function apply() + { + global $registry; + + if ($this->canApply()) { + $res = $registry->call('mail/getStream', array('INBOX')); + if ($res !== false) { + $ob = @imap_check($res); + return $this->perform(array('imap' => $res, 'mailbox' => $ob->Mailbox)); + } + } + + return false; + } + +} + +class Ingo_Script_imap_api { + + var $_params; + + function Ingo_Script_imap_api($params = array()) + { + $this->_params = $params; + } + + function factory($type, $params) + { + $class = 'Ingo_Script_imap_' . $type; + if (!class_exists($class)) { + require dirname(__FILE__) . '/imap/' . $type . '.php'; + } + return new $class($params); + } + + function deleteMessages($sequence) + { + return PEAR::raiseError('Not implemented.'); + } + + function expunge($indices) + { + return PEAR::raiseError('Not implemented.'); + } + + function moveMessages($sequence, $folder) + { + return PEAR::raiseError('Not implemented.'); + } + + function copyMessages($sequence, $folder) + { + return PEAR::raiseError('Not implemented.'); + } + + function setMessageFlags($sequence, $flags) + { + return PEAR::raiseError('Not implemented.'); + } + + function fetchMessageOverviews($sequence) + { + return PEAR::raiseError('Not implemented.'); + } + + function search($query) + { + return PEAR::raiseError('Not implemented.'); + } + + function getCache() + { + return false; + } + + function storeCache($timestamp) + { + } + + diff --git a/ingo/lib/Script/imap/live.php b/ingo/lib/Script/imap/live.php new file mode 100644 index 000000000..1c9059489 --- /dev/null +++ b/ingo/lib/Script/imap/live.php @@ -0,0 +1,122 @@ + + * @package Ingo + */ +class Ingo_Script_imap_live extends Ingo_Script_imap_api { + + /** + */ + function deleteMessages($sequence) + { + @imap_delete($this->_params['imap'], $sequence, FT_UID); + } + + /** + */ + function expunge($indices) + { + if (!count($indices)) { + return; + } + + $ids = @imap_search($this->_params['imap'], 'DELETED', SE_UID); + $unflag = false; + if (!empty($ids)) { + $unflag = array_diff($ids, $indices); + if (!empty($unflag)) { + $unflag = implode(',', $unflag); + @imap_clearflag_full($this->_params['imap'], $unflag, '\\DELETED', ST_UID); + } + } + + @imap_expunge($this->_params['imap']); + if ($unflag) { + @imap_setflag_full($this->_params['imap'], $unflag, '\\DELETED', ST_UID); + } + } + + /** + */ + function moveMessages($sequence, $folder) + { + @imap_mail_move($this->_params['imap'], $sequence, $folder, CP_UID); + } + + /** + */ + function copyMessages($sequence, $folder) + { + @imap_mail_copy($this->_params['imap'], $sequence, $folder, CP_UID); + } + + /** + */ + function setMessageFlags($sequence, $flags) + { + @imap_setflag_full($this->_params['imap'], $sequence, $flags, ST_UID); + } + + /** + */ + function fetchMessageOverviews($sequence) + { + return @imap_fetch_overview($this->_params['imap'], $sequence, FT_UID); + } + + /** + */ + function search($query) + { + $search = &Ingo_IMAP_Search::singleton($this->_params); + return $search->searchMailbox($query, $this->_params['imap'], + $this->_params['mailbox']); + } + + /** + */ + function getCache() + { + if (empty($_SESSION['ingo']['imapcache'][$this->_params['mailbox']])) { + return false; + } + $ptr = &$_SESSION['ingo']['imapcache'][$this->_params['mailbox']]; + + if ($this->_getCacheID() != $ptr['id']) { + $ptr = array(); + return false; + } + + return $ptr['ts']; + } + + /** + */ + function storeCache($timestamp) + { + if (!isset($_SESSION['ingo']['imapcache'])) { + $_SESSION['ingo']['imapcache'] = array(); + } + + $_SESSION['ingo']['imapcache'][$this->_params['mailbox']] = array( + 'id' => $this->_getCacheID(), + 'ts' => $timestamp + ); + } + + /** + */ + function _getCacheID() + { + $ob = @imap_status($this->_params['imap'], $this->_params['mailbox'], SA_ALL); + return $ob ? implode('|', array($ob->messages, $ob->uidnext, $ob->uidvalidity)) : null; + } + +} diff --git a/ingo/lib/Script/imap/mock.php b/ingo/lib/Script/imap/mock.php new file mode 100644 index 000000000..797cc2a93 --- /dev/null +++ b/ingo/lib/Script/imap/mock.php @@ -0,0 +1,117 @@ +_fixtures = array(); + + $dh = opendir($dir); + while (($dent = readdir($dh)) !== false) { + if ($dent == '.' || $dent == '..') { + continue; + } + $name = $dir . '/' . $dent; + $fh = fopen($name, 'r'); + $data = fread($fh, filesize($name)); + fclose($fh); + + $params = array('input' => &$data, + 'include_bodies' => true, + 'decode_bodies' => true, + 'decode_headers' => true); + $this->_fixtures[$dent] = Mail_mimeDecode::decode($params); + } + closedir($dh); + + $i = 0; + foreach (array_keys($this->_fixtures) as $key) { + $this->_folders['INBOX'][] = array('uid' => ++$i, + 'fixture' => $key, + 'deleted' => false); + } + } + + function hasMessage($fixture, $folder = 'INBOX') + { + if (empty($this->_folders[$folder])) { + return false; + } + foreach ($this->_folders[$folder] as $message) { + if ($message['fixture'] == $fixture) { + return !$message['deleted']; + } + } + return false; + } + + function search(&$query) + { + $result = array(); + foreach ($this->_folders['INBOX'] as $message) { + if ($message['deleted']) { + continue; + } + if ($query->matches($this->_fixtures[$message['fixture']])) { + $result[] = $message['uid']; + } + } + return $result; + } + + function deleteMessages($sequence) + { + $uids = explode(',', $sequence); + foreach (array_keys($this->_folders['INBOX']) as $i) { + if (in_array($this->_folders['INBOX'][$i]['uid'], $uids)) { + $this->_folders['INBOX'][$i]['deleted'] = true; + } + } + } + + function moveMessages($sequence, $folder) + { + $uids = explode(',', $sequence); + foreach (array_keys($this->_folders['INBOX']) as $i) { + if (in_array($this->_folders['INBOX'][$i]['uid'], $uids)) { + $this->_folders[$folder][] = $this->_folders['INBOX'][$i]; + } + } + return $this->deleteMessages($sequence); + } + + function expunge($indices) + { + foreach (array_keys($this->_folders['INBOX']) as $i) { + if (in_array($this->_folders['INBOX'][$i]['uid'], $indices)) { + unset($this->_folders['INBOX'][$i]); + } + } + + // Force renumbering + $this->_folders['INBOX'] = array_merge($this->_folders['INBOX'], + array()); + } + + function fetchMessageOverviews($sequence) + { + $result = array(); + foreach (explode(',',$sequence) as $uid) { + foreach (array_keys($this->_folders['INBOX']) as $i) { + if ($this->_folders['INBOX'][$i]['uid'] == $uid) { + $fixture = $this->_fixtures[$this->_folders['INBOX'][$i]['fixture']]; + $ob = new stdClass(); + $ob->from = $fixture->headers['from']; + $ob->uid = $uid; + $result[] = $ob; + } + } + } + return $result; + } + +} diff --git a/ingo/lib/Script/maildrop.php b/ingo/lib/Script/maildrop.php new file mode 100644 index 000000000..ac2e84b8a --- /dev/null +++ b/ingo/lib/Script/maildrop.php @@ -0,0 +1,761 @@ + + * + * See the enclosed file LICENSE for license information (ASL). If you + * did not receive this file, see http://www.horde.org/licenses/asl.php. + * + * @author Matt Weyland + * @package Ingo + */ + +/** + * Additional storage action since maildrop does not support the + * "c-flag" as in procmail. + */ +define('MAILDROP_STORAGE_ACTION_STOREANDFORWARD', 100); + +/** + */ +class Ingo_Script_maildrop extends Ingo_Script { + + /** + * The list of actions allowed (implemented) for this driver. + * + * @var array + */ + var $_actions = array( + Ingo_Storage::ACTION_KEEP, + Ingo_Storage::ACTION_MOVE, + Ingo_Storage::ACTION_DISCARD, + Ingo_Storage::ACTION_REDIRECT, + Ingo_Storage::ACTION_REDIRECTKEEP, + Ingo_Storage::ACTION_REJECT, + ); + + /** + * The categories of filtering allowed. + * + * @var array + */ + var $_categories = array( + Ingo_Storage::ACTION_BLACKLIST, + Ingo_Storage::ACTION_WHITELIST, + Ingo_Storage::ACTION_VACATION, + Ingo_Storage::ACTION_FORWARD, + Ingo_Storage::ACTION_SPAM, + ); + + /** + * The types of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_types = array( + Ingo_Storage::TYPE_HEADER, + ); + + /** + * The list of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_tests = array( + 'contains', 'not contain', + 'is', 'not is', + 'begins with','not begins with', + 'ends with', 'not ends with', + 'regex', + 'matches', 'not matches', + 'exists', 'not exist', + 'less than', 'less than or equal to', + 'equal', 'not equal', + 'greater than', 'greater than or equal to', + ); + + /** + * Can tests be case sensitive? + * + * @var boolean + */ + var $_casesensitive = true; + + /** + * Does the driver support the stop-script option? + * + * @var boolean + */ + var $_supportStopScript = false; + + /** + * Does the driver require a script file to be generated? + * + * @var boolean + */ + var $_scriptfile = true; + + /** + * The recipes that make up the code. + * + * @var array + */ + var $_recipes = array(); + + /** + * Returns a script previously generated with generate(). + * + * @return string The maildrop script. + */ + function toCode() + { + $code = ''; + foreach ($this->_recipes as $item) { + $code .= $item->generate() . "\n"; + } + return rtrim($code); + } + + /** + * Generates the maildrop script to do the filtering specified in + * the rules. + * + * @return string The maildrop script. + */ + function generate() + { + $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS); + + $this->addItem(new Maildrop_Comment(_("maildrop script generated by Ingo") . ' (' . date('F j, Y, g:i a') . ')')); + + /* Add variable information, if present. */ + if (!empty($this->_params['variables']) && + is_array($this->_params['variables'])) { + foreach ($this->_params['variables'] as $key => $val) { + $this->addItem(new Maildrop_Variable(array('name' => $key, 'value' => $val))); + } + } + + foreach ($filters->getFilterlist() as $filter) { + switch ($filter['action']) { + case Ingo_Storage::ACTION_BLACKLIST: + $this->generateBlacklist(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_WHITELIST: + $this->generateWhitelist(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_FORWARD: + $this->generateForward(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_VACATION: + $this->generateVacation(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_SPAM: + $this->generateSpamfilter(!empty($filter['disable'])); + break; + + default: + if (in_array($filter['action'], $this->_actions)) { + /* Create filter if using AND. */ + $recipe = new Maildrop_Recipe($filter, $this->_params); + foreach ($filter['conditions'] as $condition) { + $recipe->addCondition($condition); + } + $this->addItem(new Maildrop_Comment($filter['name'], !empty($filter['disable']), true)); + $this->addItem($recipe); + } + } + } + + return $this->toCode(); + } + + /** + * Generates the maildrop script to handle the blacklist specified in + * the rules. + * + * @param boolean $disable Disable the blacklist? + */ + function generateBlacklist($disable = false) + { + $blacklist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_BLACKLIST); + $bl_addr = $blacklist->getBlacklist(); + $bl_folder = $blacklist->getBlacklistFolder(); + + $bl_type = (empty($bl_folder)) ? Ingo_Storage::ACTION_DISCARD : Ingo_Storage::ACTION_MOVE; + + if (!empty($bl_addr)) { + $this->addItem(new Maildrop_Comment(_("Blacklisted Addresses"), $disable, true)); + $params = array('action-value' => $bl_folder, + 'action' => $bl_type, + 'disable' => $disable); + + foreach ($bl_addr as $address) { + if (!empty($address)) { + $recipe = new Maildrop_Recipe($params, $this->_params); + $recipe->addCondition(array('field' => 'From', 'value' => $address)); + $this->addItem($recipe); + } + } + } + } + + /** + * Generates the maildrop script to handle the whitelist specified in + * the rules. + * + * @param boolean $disable Disable the whitelist? + */ + function generateWhitelist($disable = false) + { + $whitelist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_WHITELIST); + $wl_addr = $whitelist->getWhitelist(); + + if (!empty($wl_addr)) { + $this->addItem(new Maildrop_Comment(_("Whitelisted Addresses"), $disable, true)); + foreach ($wl_addr as $address) { + if (!empty($address)) { + $recipe = new Maildrop_Recipe(array('action' => Ingo_Storage::ACTION_KEEP, 'disable' => $disable), $this->_params); + $recipe->addCondition(array('field' => 'From', 'value' => $address)); + $this->addItem($recipe); + } + } + } + } + + /** + * Generates the maildrop script to handle mail forwards. + * + * @param boolean $disable Disable forwarding? + */ + function generateForward($disable = false) + { + $forward = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FORWARD); + $addresses = $forward->getForwardAddresses(); + + if (!empty($addresses)) { + $this->addItem(new Maildrop_Comment(_("Forwards"), $disable, true)); + $params = array('action' => Ingo_Storage::ACTION_FORWARD, + 'action-value' => $addresses, + 'disable' => $disable); + if ($forward->getForwardKeep()) { + $params['action'] = MAILDROP_STORAGE_ACTION_STOREANDFORWARD; + } + $recipe = new Maildrop_Recipe($params, $this->_params); + $recipe->addCondition(array('field' => 'From', 'value' => '')); + $this->addItem($recipe); + } + } + + /** + * Generates the maildrop script to handle vacation messages. + * + * @param boolean $disable Disable forwarding? + */ + function generateVacation($disable = false) + { + $vacation = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_VACATION); + $addresses = $vacation->getVacationAddresses(); + $actionval = array('addresses' => $addresses, + 'subject' => $vacation->getVacationSubject(), + 'days' => $vacation->getVacationDays(), + 'reason' => $vacation->getVacationReason(), + 'ignorelist' => $vacation->getVacationIgnorelist(), + 'excludes' => $vacation->getVacationExcludes(), + 'start' => $vacation->getVacationStart(), + 'end' => $vacation->getVacationEnd()); + + if (!empty($addresses)) { + $this->addItem(new Maildrop_Comment(_("Vacation"), $disable, true)); + $params = array('action' => Ingo_Storage::ACTION_VACATION, + 'action-value' => $actionval, + 'disable' => $disable); + $recipe = new Maildrop_Recipe($params, $this->_params); + $this->addItem($recipe); + } + } + + /** + * Generates the maildrop script to handle spam as identified by SpamAssassin + * + * @param boolean $disable Disable the spam-filter? + */ + function generateSpamfilter($disable = false) + { + $spam = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_SPAM); + if ($spam == false) { + return; + } + + $spam_folder = $spam->getSpamFolder(); + $spam_action = (empty($spam_folder)) ? Ingo_Storage::ACTION_DISCARD : Ingo_Storage::ACTION_MOVE; + + $this->addItem(new Maildrop_Comment(_("Spam Filter"), $disable, true)); + + $params = array('action-value' => $spam_folder, + 'action' => $spam_action, + 'disable' => $disable); + $recipe = new Maildrop_Recipe($params, $this->_params); + if ($GLOBALS['conf']['spam']['compare'] == 'numeric') { + $recipe->addCondition(array('match' => 'greater than or equal to', + 'field' => $GLOBALS['conf']['spam']['header'], + 'value' => $spam->getSpamLevel())); + } elseif ($GLOBALS['conf']['spam']['compare'] == 'string') { + $recipe->addCondition(array('match' => 'contains', + 'field' => $GLOBALS['conf']['spam']['header'], + 'value' => str_repeat($GLOBALS['conf']['spam']['char'], $spam->getSpamLevel()))); + } + + $this->addItem($recipe); + } + + /** + * Adds an item to the recipe list. + * + * @param object $item The item to add to the recipe list. + * The object should have a generate() function. + */ + function addItem($item) + { + $this->_recipes[] = $item; + } + +} + +/** + * The Maildrop_Comment:: class represents a maildrop comment. This is + * a pretty simple class, but it makes the code in Ingo_Script_maildrop:: + * cleaner as it provides a generate() function and can be added to the + * recipe list the same way as a recipe can be. + * + * @author Matt Weyland + * @since Ingo 1.1 + * @package Ingo + */ +class Maildrop_Comment { + + /** + * The comment text. + * + * @var string + */ + var $_comment = ''; + + /** + * Constructs a new maildrop comment. + * + * @param string $comment Comment to be generated. + * @param boolean $disable Output 'DISABLED' comment? + * @param boolean $header Output a 'header' comment? + */ + function Maildrop_Comment($comment, $disable = false, $header = false) + { + if ($disable) { + $comment = _("DISABLED: ") . $comment; + } + + if ($header) { + $this->_comment .= "##### $comment #####"; + } else { + $this->_comment .= "# $comment"; + } + } + + /** + * Returns the comment stored by this object. + * + * @return string The comment stored by this object. + */ + function generate() + { + return $this->_comment; + } + +} + +/** + * The Maildrop_Recipe:: class represents a maildrop recipe. + * + * @author Matt Weyland + * @since Ingo 1.1 + * @package Ingo + */ +class Maildrop_Recipe { + + var $_action = array(); + var $_conditions = array(); + var $_disable = ''; + var $_flags = ''; + var $_params = array(); + var $_combine = ''; + var $_valid = true; + + var $_operators = array( + 'less than' => '<', + 'less than or equal to' => '<=', + 'equal' => '==', + 'not equal' => '!=', + 'greater than' => '>', + 'greater than or equal to' => '>=', + ); + + /** + * Constructs a new maildrop recipe. + * + * @param array $params Array of parameters. + * REQUIRED FIELDS: + * 'action' + * OPTIONAL FIELDS: + * 'action-value' (only used if the + * 'action' requires it) + * @param array $scriptparams Array of parameters passed to + * Ingo_Script_maildrop::. + */ + function Maildrop_Recipe($params = array(), $scriptparams = array()) + { + $this->_disable = !empty($params['disable']); + $this->_params = $scriptparams; + $this->_action[] = 'exception {'; + + switch ($params['action']) { + case Ingo_Storage::ACTION_KEEP: + $this->_action[] = ' to "${DEFAULT}"'; + break; + + case Ingo_Storage::ACTION_MOVE: + $this->_action[] = ' to ' . $this->maildropPath($params['action-value']); + break; + + case Ingo_Storage::ACTION_DISCARD: + $this->_action[] = ' exit'; + break; + + case Ingo_Storage::ACTION_REDIRECT: + $this->_action[] = ' to "! ' . $params['action-value'] . '"'; + break; + + case Ingo_Storage::ACTION_REDIRECTKEEP: + $this->_action[] = ' cc "! ' . $params['action-value'] . '"'; + $this->_action[] = ' to "${DEFAULT}"'; + break; + + case Ingo_Storage::ACTION_REJECT: + $this->_action[] = ' EXITCODE=77'; # EX_NOPERM (permanent failure) + $this->_action[] = ' echo "5.7.1 ' . $params['action-value'] . '"'; + $this->_action[] = ' exit'; + break; + + case Ingo_Storage::ACTION_VACATION: + $from = ''; + foreach ($params['action-value']['addresses'] as $address) { + $from = $address; + } + + /** + * @TODO + * + * Exclusion and listfilter + */ + $exclude = ''; + foreach ($params['action-value']['excludes'] as $address) { + $exclude .= $address . ' '; + } + + $start = strftime($params['action-value']['start']); + if ($start === false) { + $start = 0; + } + $end = strftime($params['action-value']['end']); + if ($end === false) { + $end = 0; + } + $days = strftime($params['action-value']['days']); + if ($days === false) { + // Set to same value as $_days in ingo/lib/Storage.php + $days = 7; + } + + // Writing vacation.msg file + $reason = Horde_Mime::encode($params['action-value']['reason'], NLS::getCharset()); + $driver = Ingo::getDriver(); + $driver->_connect(); + $result = $driver->_vfs->writeData($driver->_params['vfs_path'], 'vacation.msg', $reason, true); + + // Rule : Do not send responses to bulk or list messages + if ($params['action-value']['ignorelist'] == 1) { + $params['combine'] = Ingo_Storage::COMBINE_ALL; + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Precedence: (bulk|list|junk)/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Return-Path:.*<#@\[\]>/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Return-Path:.*<>/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^From:.*MAILER-DAEMON/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^X-ClamAV-Notice-Flag: *YES/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Content-Type:.*message\/delivery-status/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Delivery Status Notification/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Undelivered Mail Returned to Sender/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Delivery failure/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Message delay/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Mail Delivery Subsystem/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Mail System Error.*Returned Mail/')); + $this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^X-Spam-Flag: YES/ ')); + } else { + $this->addCondition(array('field' => 'From', 'value' => '')); + } + + // Rule : Start/End of vacation + if (($start != 0) && ($end !== 0)) { + $this->_action[] = ' flock "vacation.lock" {'; + $this->_action[] = ' current_time=time'; + $this->_action[] = ' if ( \ '; + $this->_action[] = ' ($current_time >= ' . $start . ') && \ '; + $this->_action[] = ' ($current_time <= ' . $end . ')) '; + $this->_action[] = ' {'; + } + $this->_action[] = " cc \"| mailbot -D " . $params['action-value']['days'] . " -c '" . NLS::getCharset() . "' -t \$HOME/vacation.msg -d \$HOME/vacation -A 'From: $from' -s '" . Horde_Mime::encode($params['action-value']['subject'], NLS::getCharset()) . "' /usr/sbin/sendmail -t \""; + if (($start != 0) && ($end !== 0)) { + $this->_action[] = ' }'; + $this->_action[] = ' }'; + } + + break; + + case Ingo_Storage::ACTION_FORWARD: + case MAILDROP_STORAGE_ACTION_STOREANDFORWARD: + foreach ($params['action-value'] as $address) { + if (!empty($address)) { + $this->_action[] = ' cc "! ' . $address . '"'; + } + } + + /* The 'to' must be the last action, because maildrop + * stops processing after it. */ + if ($params['action'] == MAILDROP_STORAGE_ACTION_STOREANDFORWARD) { + $this->_action[] = ' to "${DEFAULT}"'; + } else { + $this->_action[] = ' exit'; + } + break; + + default: + $this->_valid = false; + break; + } + + $this->_action[] = '}'; + + if (isset($params['combine']) && + ($params['combine'] == Ingo_Storage::COMBINE_ALL)) { + $this->_combine = '&& '; + } else { + $this->_combine = '|| '; + } + } + + /** + * Adds a flag to the recipe. + * + * @param string $flag String of flags to append to the current flags. + */ + function addFlag($flag) + { + $this->_flags .= $flag; + } + + /** + * Adds a condition to the recipe. + * + * @param optonal array $condition Array of parameters. Required keys + * are 'field' and 'value'. 'case' is + * an optional keys. + */ + function addCondition($condition = array()) + { + $flag = (!empty($condition['case'])) ? 'D' : ''; + if (empty($this->_conditions)) { + $this->addFlag($flag); + } + + $string = ''; + $extra = ''; + + $match = (isset($condition['match'])) ? $condition['match'] : null; + // negate tests starting with 'not ', except 'not equals', which simply uses the != operator + if ($match != 'not equal' && substr($match, 0, 4) == 'not ') { + $string .= '! '; + } + + // convert 'field' to PCRE pattern matching + if (strpos($condition['field'], ',') == false) { + $string .= '/^' . $condition['field'] . ':\\s*'; + } else { + $string .= '/^(' . str_replace(',', '|', $condition['field']) . '):\\s*'; + } + + switch ($match) { + case 'not regex': + case 'regex': + $string .= $condition['value'] . '/:h'; + break; + + case 'filter': + $string = $condition['value']; + break; + + case 'exists': + case 'not exist': + // Just run a match for the header name + $string .= '/:h'; + break; + + case 'less than or equal to': + case 'less than': + case 'equal': + case 'not equal': + case 'greater than or equal to': + case 'greater than': + $string .= '(\d+(\.\d+)?)/:h'; + $extra = ' && $MATCH1 ' . $this->_operators[$match] . ' ' . (int)$condition['value']; + break; + + case 'begins with': + case 'not begins with': + $string .= preg_quote($condition['value'], '/') . '/:h'; + break; + + case 'ends with': + case 'not ends with': + $string .= '.*' . preg_quote($condition['value'], '/') . '$/:h'; + break; + + case 'is': + case 'not is': + $string .= preg_quote($condition['value'], '/') . '$/:h'; + break; + + case 'matches': + case 'not matches': + $string .= str_replace(array('\\*', '\\?'), array('.*', '.'), preg_quote($condition['value'], '/') . '$') . '/:h'; + break; + + case 'contains': + case 'not contain': + default: + $string .= '.*' . preg_quote($condition['value'], '/') . '/:h'; + break; + } + + $this->_conditions[] = array('condition' => $string, 'flags' => $flag, 'extra' => $extra); + } + + /** + * Generates maildrop code to represent the recipe. + * + * @return string maildrop code to represent the recipe. + */ + function generate() + { + $text = array(); + + if (!$this->_valid) { + return ''; + } + + $text[] = "if( \\"; + + if (count($this->_conditions > 1)) { + $nest = false; + foreach ($this->_conditions as $condition) { + $cond = $nest ? $this->_combine : ' '; + $text[] = $cond . $condition['condition'] . $condition['flags'] . $condition['extra'] . " \\"; + $nest = true; + } + } + + $text[] = ')'; + + foreach ($this->_action as $val) { + $text[] = $val; + } + + if ($this->_disable) { + $code = ''; + foreach ($text as $val) { + $comment = new Maildrop_Comment($val); + $code .= $comment->generate() . "\n"; + } + return $code . "\n"; + } else { + return implode("\n", $text) . "\n"; + } + } + + /** + * Returns a maildrop-ready mailbox path, converting IMAP folder pathname + * conventions as necessary. + * + * @param string $folder The IMAP folder name. + * + * @return string The maildrop mailbox path. + */ + function maildropPath($folder) + { + /* NOTE: '$DEFAULT' here is a literal, not a PHP variable. */ + if (isset($this->_params) && + ($this->_params['path_style'] == 'maildir')) { + if (empty($folder) || ($folder == 'INBOX')) { + return '"${DEFAULT}"'; + } + if ($this->_params['strip_inbox'] && + substr($folder, 0, 6) == 'INBOX.') { + $folder = substr($folder, 6); + } + return '"${DEFAULT}/.' . $folder . '/"'; + } else { + if (empty($folder) || ($folder == 'INBOX')) { + return '${DEFAULT}'; + } + return str_replace(' ', '\ ', $folder); + } + } + +} + +/** + * The Maildrop_Variable:: class represents a Maildrop variable. + * + * @author Matt Weyland + * @since Ingo 1.1 + * @package Ingo + */ +class Maildrop_Variable { + + var $_name; + var $_value; + + /** + * Constructs a new maildrop variable. + * + * @param array $params Array of parameters. Expected fields are 'name' + * and 'value'. + */ + function Maildrop_Variable($params = array()) + { + $this->_name = $params['name']; + $this->_value = $params['value']; + } + + /** + * Generates maildrop code to represent the variable. + * + * @return string maildrop code to represent the variable. + */ + function generate() + { + return $this->_name . '=' . $this->_value . "\n"; + } + +} diff --git a/ingo/lib/Script/procmail.php b/ingo/lib/Script/procmail.php new file mode 100644 index 000000000..ed354091e --- /dev/null +++ b/ingo/lib/Script/procmail.php @@ -0,0 +1,802 @@ + + * @author Ben Chavet + * @package Ingo + */ +class Ingo_Script_procmail extends Ingo_Script { + + /** + * The list of actions allowed (implemented) for this driver. + * + * @var array + */ + var $_actions = array( + Ingo_Storage::ACTION_KEEP, + Ingo_Storage::ACTION_MOVE, + Ingo_Storage::ACTION_DISCARD, + Ingo_Storage::ACTION_REDIRECT, + Ingo_Storage::ACTION_REDIRECTKEEP, + Ingo_Storage::ACTION_REJECT + ); + + /** + * The categories of filtering allowed. + * + * @var array + */ + var $_categories = array( + Ingo_Storage::ACTION_BLACKLIST, + Ingo_Storage::ACTION_WHITELIST, + Ingo_Storage::ACTION_VACATION, + Ingo_Storage::ACTION_FORWARD + ); + + /** + * The types of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_types = array( + Ingo_Storage::TYPE_HEADER, + Ingo_Storage::TYPE_BODY + ); + + /** + * A list of any special types that this driver supports. + * + * @var array + */ + var $_special_types = array( + 'Destination', + ); + + /** + * The list of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_tests = array( + 'contains', + 'not contain', + 'begins with', + 'not begins with', + 'ends with', + 'not ends with', + 'regex' + ); + + /** + * Can tests be case sensitive? + * + * @var boolean + */ + var $_casesensitive = true; + + /** + * Does the driver support the stop-script option? + * + * @var boolean + */ + var $_supportStopScript = true; + + /** + * Does the driver require a script file to be generated? + * + * @var boolean + */ + var $_scriptfile = true; + + /** + * The recipes that make up the code. + * + * @var array + */ + var $_recipes = array(); + + /** + * Returns a script previously generated with generate(). + * + * @return string The procmail script. + */ + function toCode() + { + $code = ''; + foreach ($this->_recipes as $item) { + $code .= $item->generate() . "\n"; + } + + // If an external delivery program is used, add final rule + // to deliver to $DEFAULT + if (isset($this->_params['delivery_agent'])) { + $code .= ":0 w\n"; + $code .= isset($this->_params['delivery_mailbox_prefix']) ? + '| ' . $this->_params['delivery_agent'] . ' ' . $this->_params['delivery_mailbox_prefix'] . '$DEFAULT' : + '| ' . $this->_params['delivery_agent'] . ' $DEFAULT'; + } + + return rtrim($code) . "\n"; + } + + /** + * Generates the procmail script to do the filtering specified in the + * rules. + * + * @return string The procmail script. + */ + function generate() + { + $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS); + + $this->addItem(new Procmail_Comment(_("procmail script generated by Ingo") . ' (' . date('F j, Y, g:i a') . ')')); + + /* Add variable information, if present. */ + if (!empty($this->_params['variables']) && + is_array($this->_params['variables'])) { + foreach ($this->_params['variables'] as $key => $val) { + $this->addItem(new Procmail_Variable(array('name' => $key, 'value' => $val))); + } + } + + foreach ($filters->getFilterlist() as $filter) { + switch ($filter['action']) { + case Ingo_Storage::ACTION_BLACKLIST: + $this->generateBlacklist(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_WHITELIST: + $this->generateWhitelist(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_VACATION: + $this->generateVacation(!empty($filter['disable'])); + break; + + case Ingo_Storage::ACTION_FORWARD: + $this->generateForward(!empty($filter['disable'])); + break; + + default: + if (in_array($filter['action'], $this->_actions)) { + /* Create filter if using AND. */ + if ($filter['combine'] == Ingo_Storage::COMBINE_ALL) { + $recipe = new Procmail_Recipe($filter, $this->_params); + if (!$filter['stop']) { + $recipe->addFlag('c'); + } + foreach ($filter['conditions'] as $condition) { + $recipe->addCondition($condition); + } + $this->addItem(new Procmail_Comment($filter['name'], !empty($filter['disable']), true)); + $this->addItem($recipe); + } else { + /* Create filter if using OR */ + $this->addItem(new Procmail_Comment($filter['name'], !empty($filter['disable']), true)); + $loop = 0; + foreach ($filter['conditions'] as $condition) { + $recipe = &new Procmail_Recipe($filter, $this->_params); + if ($loop++) { + $recipe->addFlag('E'); + } + if (!$filter['stop']) { + $recipe->addFlag('c'); + } + $recipe->addCondition($condition); + $this->addItem($recipe); + } + } + } + } + } + + return $this->toCode(); + } + + /** + * Generates the procmail script to handle the blacklist specified in + * the rules. + * + * @param boolean $disable Disable the blacklist? + */ + function generateBlacklist($disable = false) + { + $blacklist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_BLACKLIST); + $bl_addr = $blacklist->getBlacklist(); + $bl_folder = $blacklist->getBlacklistFolder(); + + $bl_type = (empty($bl_folder)) ? Ingo_Storage::ACTION_DISCARD : Ingo_Storage::ACTION_MOVE; + + if (!empty($bl_addr)) { + $this->addItem(new Procmail_Comment(_("Blacklisted Addresses"), $disable, true)); + $params = array('action-value' => $bl_folder, + 'action' => $bl_type, + 'disable' => $disable); + + foreach ($bl_addr as $address) { + if (!empty($address)) { + $recipe = new Procmail_Recipe($params, $this->_params); + $recipe->addCondition(array('field' => 'From', 'value' => $address, 'match' => 'address')); + $this->addItem($recipe); + } + } + } + } + + /** + * Generates the procmail script to handle the whitelist specified in + * the rules. + * + * @param boolean $disable Disable the whitelist? + */ + function generateWhitelist($disable = false) + { + $whitelist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_WHITELIST); + $wl_addr = $whitelist->getWhitelist(); + + if (!empty($wl_addr)) { + $this->addItem(new Procmail_Comment(_("Whitelisted Addresses"), $disable, true)); + foreach ($wl_addr as $address) { + if (!empty($address)) { + $recipe = new Procmail_Recipe(array('action' => Ingo_Storage::ACTION_KEEP, 'disable' => $disable), $this->_params); + $recipe->addCondition(array('field' => 'From', 'value' => $address, 'match' => 'address')); + $this->addItem($recipe); + } + } + } + } + + /** + * Generates the procmail script to handle vacation. + * + * @param boolean $disable Disable vacation? + */ + function generateVacation($disable = false) + { + $vacation = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_VACATION); + $addresses = $vacation->getVacationAddresses(); + $actionval = array( + 'addresses' => $addresses, + 'subject' => $vacation->getVacationSubject(), + 'days' => $vacation->getVacationDays(), + 'reason' => $vacation->getVacationReason(), + 'ignorelist' => $vacation->getVacationIgnorelist(), + 'excludes' => $vacation->getVacationExcludes(), + 'start' => $vacation->getVacationStart(), + 'end' => $vacation->getVacationEnd(), + ); + + if (!empty($addresses)) { + $this->addItem(new Procmail_Comment(_("Vacation"), $disable, true)); + $params = array('action' => Ingo_Storage::ACTION_VACATION, + 'action-value' => $actionval, + 'disable' => $disable); + $recipe = new Procmail_Recipe($params, $this->_params); + $this->addItem($recipe); + } + } + + /** + * Generates the procmail script to handle mail forwards. + * + * @param boolean $disable Disable forwarding? + */ + function generateForward($disable = false) + { + $forward = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FORWARD); + $addresses = $forward->getForwardAddresses(); + + if (!empty($addresses)) { + $this->addItem(new Procmail_Comment(_("Forwards"), $disable, true)); + $params = array('action' => Ingo_Storage::ACTION_FORWARD, + 'action-value' => $addresses, + 'disable' => $disable); + $recipe = new Procmail_Recipe($params, $this->_params); + if ($forward->getForwardKeep()) { + $recipe->addFlag('c'); + } + $this->addItem($recipe); + } + } + + /** + * Adds an item to the recipe list. + * + * @param object $item The item to add to the recipe list. + * The object should have a generate() function. + */ + function addItem($item) + { + $this->_recipes[] = $item; + } + +} + +/** + * The Procmail_Comment:: class represents a Procmail comment. This is + * a pretty simple class, but it makes the code in Ingo_Script_procmail:: + * cleaner as it provides a generate() function and can be added to the + * recipe list the same way as a recipe can be. + * + * @author Ben Chavet + * @since Ingo 1.0 + * @package Ingo + */ +class Procmail_Comment { + + /** + * The comment text. + * + * @var string + */ + var $_comment = ''; + + /** + * Constructs a new procmail comment. + * + * @param string $comment Comment to be generated. + * @param boolean $disable Output 'DISABLED' comment? + * @param boolean $header Output a 'header' comment? + */ + function Procmail_Comment($comment, $disable = false, $header = false) + { + if ($disable) { + $comment = _("DISABLED: ") . $comment; + } + + if ($header) { + $this->_comment .= "##### $comment #####"; + } else { + $this->_comment .= "# $comment"; + } + } + + /** + * Returns the comment stored by this object. + * + * @return string The comment stored by this object. + */ + function generate() + { + return $this->_comment; + } + +} + +/** + * The Procmail_Recipe:: class represents a Procmail recipe. + * + * @author Ben Chavet + * @since Ingo 1.0 + * @package Ingo + */ +class Procmail_Recipe { + + var $_action = array(); + var $_conditions = array(); + var $_disable = ''; + var $_flags = ''; + var $_params = array( + 'date' => 'date', + 'echo' => 'echo', + 'ls' => 'ls' + ); + var $_valid = true; + + /** + * Constructs a new procmail recipe. + * + * @param array $params Array of parameters. + * REQUIRED FIELDS: + * 'action' + * OPTIONAL FIELDS: + * 'action-value' (only used if the + * 'action' requires it) + * @param array $scriptparams Array of parameters passed to + * Ingo_Script_procmail::. + */ + function Procmail_Recipe($params = array(), $scriptparams = array()) + { + $this->_disable = !empty($params['disable']); + $this->_params = array_merge($this->_params, $scriptparams); + + switch ($params['action']) { + case Ingo_Storage::ACTION_KEEP: + // Note: you may have to set the DEFAULT variable in your + // backend configuration. + if (isset($this->_params['delivery_agent']) && isset($this->_params['delivery_mailbox_prefix'])) { + $this->_action[] = '| ' . $this->_params['delivery_agent'] . ' ' . $this->_params['delivery_mailbox_prefix'] . '$DEFAULT'; + } elseif (isset($this->_params['delivery_agent'])) { + $this->_action[] = '| ' . $this->_params['delivery_agent'] . ' $DEFAULT'; + } else { + $this->_action[] = '$DEFAULT'; + } + break; + + case Ingo_Storage::ACTION_MOVE: + if (isset($this->_params['delivery_agent']) && isset($this->_params['delivery_mailbox_prefix'])) { + $this->_action[] = '| ' . $this->_params['delivery_agent'] . ' ' . $this->_params['delivery_mailbox_prefix'] . $this->procmailPath($params['action-value']); + } elseif (isset($this->_params['delivery_agent'])) { + $this->_action[] = '| ' . $this->_params['delivery_agent'] . ' ' . $this->procmailPath($params['action-value']); + } else { + $this->_action[] = $this->procmailPath($params['action-value']); + } + break; + + case Ingo_Storage::ACTION_DISCARD: + $this->_action[] = '/dev/null'; + break; + + case Ingo_Storage::ACTION_REDIRECT: + $this->_action[] = '! ' . $params['action-value']; + break; + + case Ingo_Storage::ACTION_REDIRECTKEEP: + $this->_action[] = '{'; + $this->_action[] = ' :0 c'; + $this->_action[] = ' ! ' . $params['action-value']; + $this->_action[] = ''; + $this->_action[] = ' :0' . (isset($this->_params['delivery_agent']) ? ' w' : ''); + if (isset($this->_params['delivery_agent']) && isset($this->_params['delivery_mailbox_prefix'])) { + $this->_action[] = ' | ' . $this->_params['delivery_agent'] . ' ' . $this->_params['delivery_mailbox_prefix'] . '$DEFAULT'; + } elseif (isset($this->_params['delivery_agent'])) { + $this->_action[] = ' | ' . $this->_params['delivery_agent'] . ' $DEFAULT'; + } else { + $this->_action[] = ' $DEFAULT'; + } + $this->_action[] = '}'; + break; + + case Ingo_Storage::ACTION_REJECT: + $this->_action[] = '{'; + $this->_action[] = ' EXITCODE=' . $params['action-value']; + $this->_action[] = ' HOST="no.address.here"'; + $this->_action[] = '}'; + break; + + case Ingo_Storage::ACTION_VACATION: + $days = $params['action-value']['days']; + $timed = !empty($params['action-value']['start']) && + !empty($params['action-value']['end']); + $this->_action[] = '{'; + foreach ($params['action-value']['addresses'] as $address) { + if (!empty($address)) { + $this->_action[] = ' FILEDATE=`test -f ${VACATION_DIR:-.}/\'.vacation.' . $address . '\' && ' + . $this->_params['ls'] . ' -lcn --time-style=+%s ${VACATION_DIR:-.}/\'.vacation.' . $address . '\' | ' + . 'awk \'{ print $6 + (' . $days * 86400 . ') }\'`'; + $this->_action[] = ' DATE=`' . $this->_params['date'] . ' +%s`'; + $this->_action[] = ' DUMMY=`test -f ${VACATION_DIR:-.}/\'.vacation.' . $address . '\' && ' + . 'test $FILEDATE -le $DATE && ' + . 'rm ${VACATION_DIR:-.}/\'.vacation.' . $address . '\'`'; + if ($timed) { + $this->_action[] = ' START=' . $params['action-value']['start']; + $this->_action[] = ' END=' . $params['action-value']['end']; + } + $this->_action[] = ''; + $this->_action[] = ' :0 h'; + $this->_action[] = ' SUBJECT=| formail -xSubject:'; + $this->_action[] = ''; + $this->_action[] = ' :0 Whc: ${VACATION_DIR:-.}/vacation.lock'; + if ($timed) { + $this->_action[] = ' * ? test $DATE -gt $START && test $END -gt $DATE'; + $this->_action[] = ' {'; + $this->_action[] = ' :0 Whc'; + } + $this->_action[] = ' * ^TO_' . $address; + $this->_action[] = ' * !^X-Loop: ' . $address; + $this->_action[] = ' * !^X-Spam-Flag: YES'; + if (count($params['action-value']['excludes']) > 0) { + foreach ($params['action-value']['excludes'] as $exclude) { + if (!empty($exclude)) { + $this->_action[] = ' * !^From.*' . $exclude; + } + } + } + if ($params['action-value']['ignorelist']) { + $this->_action[] = ' * !^FROM_DAEMON'; + } + $this->_action[] = ' | formail -rD 8192 ${VACATION_DIR:-.}/.vacation.' . $address; + $this->_action[] = ' :0 ehc'; + $this->_action[] = ' | (formail -rI"Precedence: junk" \\'; + $this->_action[] = ' -a"From: <' . $address . '>" \\'; + $this->_action[] = ' -A"X-Loop: ' . $address . '" \\'; + if (Horde_Mime::is8bit($params['action-value']['reason'])) { + $this->_action[] = ' -i"Subject: ' . Horde_Mime::encode($params['action-value']['subject'] . ' (Re: $SUBJECT)', NLS::getCharset()) . '" \\'; + $this->_action[] = ' -i"Content-Transfer-Encoding: quoted-printable" \\'; + $this->_action[] = ' -i"Content-Type: text/plain; charset=' . NLS::getCharset() . '" ; \\'; + $reason = Horde_Mime::quotedPrintableEncode($params['action-value']['reason'], "\n"); + } else { + $this->_action[] = ' -i"Subject: ' . Horde_Mime::encode($params['action-value']['subject'] . ' (Re: $SUBJECT)', NLS::getCharset()) . '" ; \\'; + $reason = $params['action-value']['reason']; + } + $reason = addcslashes($reason, "\\\n\r\t\"`"); + $this->_action[] = ' ' . $this->_params['echo'] . ' -e "' . $reason . '" \\'; + $this->_action[] = ' ) | $SENDMAIL -f' . $address . ' -oi -t'; + $this->_action[] = ''; + $this->_action[] = ' :0'; + $this->_action[] = ' /dev/null'; + if ($timed) { + $this->_action[] = ' }'; + } + } + } + $this->_action[] = '}'; + break; + + case Ingo_Storage::ACTION_FORWARD: + /* Make sure that we prevent mail loops using 3 methods. + * + * First, we call sendmail -f to set the envelope sender to be the + * same as the original sender, so bounces will go to the original + * sender rather than to us. This unfortunately triggers lots of + * Authentication-Warning: messages in sendmail's logs. + * + * Second, add an X-Loop header, to handle the case where the + * address we forward to forwards back to us. + * + * Third, don't forward mailer daemon messages (i.e., bounces). + * Method 1 above should make this redundant, unless we're sending + * mail from this account and have a bad forward-to account. + * + * Get the from address, saving a call to formail if possible. + * The procmail code for doing this is borrowed from the + * Procmail Library Project, http://pm-lib.sourceforge.net/. + * The Ingo project has the permission to use Procmail Library code + * under Apache licence v 1.x or any later version. + * Permission obtained 2006-04-04 from Author Jari Aalto. */ + $this->_action[] = '{'; + $this->_action[] = ' :0 '; + $this->_action[] = ' *$ ! ^From *\/[^ ]+'; + $this->_action[] = ' *$ ! ^Sender: *\/[^ ]+'; + $this->_action[] = ' *$ ! ^From: *\/[^ ]+'; + $this->_action[] = ' *$ ! ^Reply-to: *\/[^ ]+'; + $this->_action[] = ' {'; + $this->_action[] = ' OUTPUT = `formail -zxFrom:`'; + $this->_action[] = ' }'; + $this->_action[] = ' :0 E'; + $this->_action[] = ' {'; + $this->_action[] = ' OUTPUT = $MATCH'; + $this->_action[] = ' }'; + $this->_action[] = ''; + + /* Forward to each address on our list. */ + foreach ($params['action-value'] as $address) { + if (!empty($address)) { + $this->_action[] = ' :0 c'; + $this->_action[] = ' * !^FROM_MAILER'; + $this->_action[] = ' * !^X-Loop: to-' . $address; + $this->_action[] = ' | formail -A"X-Loop: to-' . $address . '" | $SENDMAIL -oi -f $OUTPUT ' . $address; + } + } + + /* In case of mail loop or bounce, store a copy locally. Note + * that if we forward to more than one address, only a mail loop + * on the last address will cause a local copy to be saved. TODO: + * The next two lines are redundant (and create an extra copy of + * the message) if "Keep a copy of messages in this account" is + * checked. */ + $this->_action[] = ' :0 E' . (isset($this->_params['delivery_agent']) ? 'w' : ''); + if (isset($this->_params['delivery_agent'])) { + $this->_action[] = isset($this->_params['delivery_mailbox_prefix']) ? + ' | ' . $this->_params['delivery_agent'] . ' ' . $this->_params['delivery_mailbox_prefix'] . '$DEFAULT' : + ' | ' . $this->_params['delivery_agent'] . ' $DEFAULT'; + } else { + $this->_action[] = ' $DEFAULT'; + } + $this->_action[] = ' :0 '; + $this->_action[] = ' /dev/null'; + $this->_action[] = '}'; + break; + + default: + $this->_valid = false; + break; + } + } + + /** + * Adds a flag to the recipe. + * + * @param string $flag String of flags to append to the current flags. + */ + function addFlag($flag) + { + $this->_flags .= $flag; + } + + /** + * Adds a condition to the recipe. + * + * @param array $condition Array of parameters. Required keys are 'field' + * and 'value'. 'case' is an optional key. + */ + function addCondition($condition = array()) + { + $flag = !empty($condition['case']) ? 'D' : ''; + $match = isset($condition['match']) ? $condition['match'] : null; + $string = ''; + $prefix = ''; + + switch ($condition['field']) { + case 'Destination': + $string = '^TO_'; + break; + + case 'Body': + $flag .= 'B'; + break; + + default: + // convert 'field' to PCRE pattern matching + if (strpos($condition['field'], ',') == false) { + $string = '^' . $condition['field'] . ':'; + } else { + $string .= '/^(' . str_replace(',', '|', $condition['field']) . '):'; + } + $prefix = ' '; + } + + $reverseCondition = false; + switch ($match) { + case 'regex': + $string .= $prefix . $condition['value']; + break; + + case 'address': + $string .= '(.*\<)?' . quotemeta($condition['value']); + break; + + case 'not begins with': + $reverseCondition = true; + // fall through + case 'begins with': + $string .= $prefix . quotemeta($condition['value']); + break; + + case 'not ends with': + $reverseCondition = true; + // fall through + case 'ends with': + $string .= '.*' . quotemeta($condition['value']) . '$'; + break; + + case 'not contain': + $reverseCondition = true; + // fall through + case 'contains': + default: + $string .= '.*' . quotemeta($condition['value']); + break; + } + + $this->_conditions[] = array('condition' => ($reverseCondition ? '* !' : '* ') . $string, + 'flags' => $flag); + } + + /** + * Generates procmail code to represent the recipe. + * + * @return string Procmail code to represent the recipe. + */ + function generate() + { + $nest = 0; + $prefix = ''; + $text = array(); + + if (!$this->_valid) { + return ''; + } + + // Set the global flags for the whole rule, each condition + // will add its own (such as Body or Case Sensitive) + $global = $this->_flags; + if (isset($this->_conditions[0])) { + $global .= $this->_conditions[0]['flags']; + } + $text[] = ':0 ' . $global . (isset($this->_params['delivery_agent']) ? 'w' : ''); + foreach ($this->_conditions as $condition) { + if ($nest > 0) { + $text[] = str_repeat(' ', $nest - 1) . '{'; + $text[] = str_repeat(' ', $nest) . ':0 ' . $condition['flags']; + $text[] = str_repeat(' ', $nest) . $condition['condition']; + } else { + $text[] = $condition['condition']; + } + $nest++; + } + + if (--$nest > 0) { + $prefix = str_repeat(' ', $nest); + } + foreach ($this->_action as $val) { + $text[] = $prefix . $val; + } + + for ($i = $nest; $i > 0; $i--) { + $text[] = str_repeat(' ', $i - 1) . '}'; + } + + if ($this->_disable) { + $code = ''; + foreach ($text as $val) { + $comment = new Procmail_Comment($val); + $code .= $comment->generate() . "\n"; + } + return $code . "\n"; + } else { + return implode("\n", $text) . "\n"; + } + } + + /** + * Returns a procmail-ready mailbox path, converting IMAP folder + * pathname conventions as necessary. + * + * @param string $folder The IMAP folder name. + * + * @return string The procmail mailbox path. + */ + function procmailPath($folder) + { + /* NOTE: '$DEFAULT' here is a literal, not a PHP variable. */ + if (isset($this->_params) && + ($this->_params['path_style'] == 'maildir')) { + if (empty($folder) || ($folder == 'INBOX')) { + return '$DEFAULT'; + } + if (substr($folder, 0, 6) == 'INBOX.') { + $folder = substr($folder, 6); + } + return '"$DEFAULT/.' . escapeshellcmd($folder) . '/"'; + } else { + if (empty($folder) || ($folder == 'INBOX')) { + return '$DEFAULT'; + } + return str_replace(' ', '\ ', escapeshellcmd($folder)); + } + } + +} + +/** + * The Procmail_Variable:: class represents a Procmail variable. + * + * @author Michael Slusarz + * @since Ingo 1.0 + * @package Ingo + */ +class Procmail_Variable { + + var $_name; + var $_value; + + /** + * Constructs a new procmail variable. + * + * @param array $params Array of parameters. Expected fields are 'name' + * and 'value'. + */ + function Procmail_Variable($params = array()) + { + $this->_name = $params['name']; + $this->_value = $params['value']; + } + + /** + * Generates procmail code to represent the variable. + * + * @return string Procmail code to represent the variable. + */ + function generate() + { + return $this->_name . '=' . $this->_value . "\n"; + } + +} diff --git a/ingo/lib/Script/sieve.php b/ingo/lib/Script/sieve.php new file mode 100644 index 000000000..1995e114c --- /dev/null +++ b/ingo/lib/Script/sieve.php @@ -0,0 +1,2976 @@ + + * @package Ingo + */ +class Ingo_Script_sieve extends Ingo_Script { + + /** + * The list of actions allowed (implemented) for this driver. + * + * @var array + */ + var $_actions = array( + Ingo_Storage::ACTION_KEEP, + Ingo_Storage::ACTION_MOVE, + Ingo_Storage::ACTION_DISCARD, + Ingo_Storage::ACTION_REDIRECT, + Ingo_Storage::ACTION_REDIRECTKEEP, + Ingo_Storage::ACTION_MOVEKEEP, + Ingo_Storage::ACTION_REJECT, + Ingo_Storage::ACTION_FLAGONLY, + Ingo_Storage::ACTION_NOTIFY + ); + + /** + * The categories of filtering allowed. + * + * @var array + */ + var $_categories = array( + Ingo_Storage::ACTION_BLACKLIST, + Ingo_Storage::ACTION_WHITELIST, + Ingo_Storage::ACTION_VACATION, + Ingo_Storage::ACTION_FORWARD, + Ingo_Storage::ACTION_SPAM + ); + + /** + * The list of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_tests = array( + 'contains', 'not contain', 'is', 'not is', 'begins with', + 'not begins with', 'ends with', 'not ends with', 'exists', 'not exist', + 'less than', 'less than or equal to', 'equal', 'not equal', + 'greater than', 'greater than or equal to', 'regex', 'matches', + 'not matches' + ); + + /** + * The types of tests allowed (implemented) for this driver. + * + * @var array + */ + var $_types = array( + Ingo_Storage::TYPE_HEADER, + Ingo_Storage::TYPE_SIZE, + Ingo_Storage::TYPE_BODY + ); + + /** + * Can tests be case sensitive? + * + * @var boolean + */ + var $_casesensitive = true; + + /** + * Does the driver support setting IMAP flags? + * + * @var boolean + */ + var $_supportIMAPFlags = true; + + /** + * Does the driver support the stop-script option? + * + * @var boolean + */ + var $_supportStopScript = true; + + /** + * Does the driver require a script file to be generated? + * + * @var boolean + */ + var $_scriptfile = true; + + /** + * The blocks that make up the code. + * + * @var array + */ + var $_blocks = array(); + + /** + * The blocks that have to appear at the end of the code. + * + * @var array + */ + var $_endBlocks = array(); + + /** + * Returns a script previously generated with generate(). + * + * @return string The Sieve script. + */ + function toCode() + { + $date_format = $GLOBALS['prefs']->getValue('date_format'); + // %R and %r don't work on Windows, but who runs a Sieve backend on a + // Windows server? + $time_format = $GLOBALS['prefs']->getValue('twentyFour') ? '%R' : '%r'; + $code = "# Sieve Filter\n# " + . _("Generated by Ingo (http://www.horde.org/ingo/)") . ' (' + . trim(strftime($date_format . ', ' . $time_format)) + . ")\n\n"; + $code = String::convertCharset($code, NLS::getCharset(), 'UTF-8'); + $requires = $this->requires(); + + if (count($requires) > 1) { + $stringlist = ''; + foreach ($this->requires() as $require) { + $stringlist .= (empty($stringlist)) ? '"' : ', "'; + $stringlist .= $require . '"'; + } + $code .= 'require [' . $stringlist . '];' . "\n\n"; + } elseif (count($requires) == 1) { + foreach ($this->requires() as $require) { + $code .= 'require "' . $require . '";' . "\n\n"; + } + } + + foreach ($this->_blocks as $block) { + $code .= $block->toCode() . "\n"; + } + + return rtrim($code) . "\n"; + } + + /** + * Escape a string according to Sieve RFC 3028 [2.4.2]. + * + * @param string $string The string to escape. + * @param boolean $regexmode Is the escaped string a regex value? + * Defaults to no. + * + * @return string The escaped string. + */ + function escapeString($string, $regexmode = false) + { + /* Remove any backslashes in front of commas. */ + $string = str_replace('\,', ',', $string); + + if ($regexmode) { + return str_replace('"', addslashes('"'), $string); + } else { + return str_replace(array('\\', '"'), array(addslashes('\\'), addslashes('"')), $string); + } + } + + /** + * Checks if all rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + foreach ($this->_blocks as $block) { + $res = $block->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + foreach ($this->_blocks as $block) { + $requires = array_merge($requires, $block->requires()); + } + + return array_unique($requires); + } + + /** + * Adds all blocks necessary for the forward rule. + */ + function _addForwardBlocks() + { + if (!$this->_validRule(Ingo_Storage::ACTION_FORWARD)) { + return; + } + + $forward = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FORWARD); + $fwd_addr = $forward->getForwardAddresses(); + if (empty($fwd_addr)) { + return; + } + + $action = array(); + foreach ($fwd_addr as $addr) { + $addr = trim($addr); + if (!empty($addr)) { + $action[] = new Sieve_Action_Redirect(array('address' => $addr)); + } + } + + if (count($action)) { + if($forward->getForwardKeep()) { + $this->_endBlocks[] = new Sieve_Comment(_("Forward Keep Action")); + $if = new Sieve_If(new Sieve_Test_True()); + $if->setActions(array(new Sieve_Action_Keep(), + new Sieve_Action_Stop())); + $this->_endBlocks[] = $if; + } else { + $action[] = new Sieve_Action_Stop(); + } + } + + $this->_blocks[] = new Sieve_Comment(_("Forwards")); + + $test = new Sieve_Test_True(); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } + + /** + * Adds all blocks necessary for the blacklist rule. + */ + function _addBlacklistBlocks() + { + if (!$this->_validRule(Ingo_Storage::ACTION_BLACKLIST)) { + return; + } + + $blacklist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_BLACKLIST); + $bl_addr = $blacklist->getBlacklist(); + $folder = $blacklist->getBlacklistFolder(); + if (empty($bl_addr)) { + return; + } + + $action = array(); + if (empty($folder)) { + $action[] = new Sieve_Action_Discard(); + } elseif ($folder == INGO_BLACKLIST_MARKER) { + $action[] = new Sieve_Action_Addflag(array('flags' => Ingo_Storage::FLAG_DELETED)); + $action[] = new Sieve_Action_Keep(); + $action[] = new Sieve_Action_Removeflag(array('flags' => Ingo_Storage::FLAG_DELETED)); + } else { + $action[] = new Sieve_Action_Fileinto(array('folder' => $folder)); + } + + $action[] = new Sieve_Action_Stop(); + + $this->_blocks[] = new Sieve_Comment(_("Blacklisted Addresses")); + + /* Split the test up to only do 5 addresses at a time. */ + $temp = array(); + $wildcards = array(); + foreach ($bl_addr as $address) { + if (!empty($address)) { + if ((strstr($address, '*') !== false) || + (strstr($address, '?') !== false)) { + $wildcards[] = $address; + } else { + $temp[] = $address; + } + } + if (count($temp) == 5) { + $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $temp))); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + $temp = array(); + } + if (count($wildcards) == 5) { + $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'match-type' => ':matches', 'addresses' => implode("\n", $wildcards))); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + $wildcards = array(); + } + } + + if ($temp) { + $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $temp))); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } + + if ($wildcards) { + $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'match-type' => ':matches', 'addresses' => implode("\n", $wildcards))); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } + } + + /** + * Adds all blocks necessary for the whitelist rule. + */ + function _addWhitelistBlocks() + { + if (!$this->_validRule(Ingo_Storage::ACTION_WHITELIST)) { + return; + } + + $whitelist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_WHITELIST); + $wl_addr = $whitelist->getWhitelist(); + if (empty($wl_addr)) { + return; + } + + $this->_blocks[] = new Sieve_Comment(_("Whitelisted Addresses")); + + $action = array(new Sieve_Action_Keep(), new Sieve_Action_Stop()); + $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $wl_addr))); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } + + /** + * Adds all blocks necessary for the vacation rule. + */ + function _addVacationBlocks() + { + if (!$this->_validRule(Ingo_Storage::ACTION_VACATION)) { + return; + } + + $vacation = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_VACATION); + $vacation_addr = $vacation->getVacationAddresses(); + if (!count($vacation_addr)) { + return; + } + + $vals = array( + 'subject' => String::convertCharset($vacation->getVacationSubject(), NLS::getCharset(), 'UTF-8'), + 'days' => $vacation->getVacationDays(), + 'addresses' => $vacation_addr, + 'start' => $vacation->getVacationStart(), + 'start_year' => $vacation->getVacationStartYear(), + 'start_month' => $vacation->getVacationStartMonth(), + 'start_day' => $vacation->getVacationStartDay(), + 'end' => $vacation->getVacationEnd(), + 'end_year' => $vacation->getVacationEndYear(), + 'end_month' => $vacation->getVacationEndMonth(), + 'end_day' => $vacation->getVacationEndDay(), + 'reason' => String::convertCharset($vacation->getVacationReason(), NLS::getCharset(), 'UTF-8'), + ); + + $action = $tests = array(); + $action[] = new Sieve_Action_Vacation($vals); + + if ($vacation->getVacationIgnorelist()) { + $mime_headers = new Horde_Mime_Headers(); + $headers = $mime_headers->listHeaders(); + $headers['Mailing-List'] = null; + $tmp = new Sieve_Test_Exists(array('headers' => implode("\n", array_keys($headers)))); + $tests[] = new Sieve_Test_Not($tmp); + $vals = array('headers' => 'Precedence', + 'match-type' => ':is', + 'strings' => "list\nbulk\njunk", + 'comparator' => 'i;ascii-casemap'); + $tmp = new Sieve_Test_Header($vals); + $tests[] = new Sieve_Test_Not($tmp); + $vals = array('headers' => 'To', + 'match-type' => ':matches', + 'strings' => 'Multiple recipients of*', + 'comparator' => 'i;ascii-casemap'); + $tmp = new Sieve_Test_Header($vals); + $tests[] = new Sieve_Test_Not($tmp); + } + + $addrs = array(); + foreach ($vacation->getVacationExcludes() as $addr) { + $addr = trim($addr); + if (!empty($addr)) { + $addrs[] = $addr; + } + } + + if ($addrs) { + $tmp = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $addrs))); + $tests[] = new Sieve_Test_Not($tmp); + } + + $this->_blocks[] = new Sieve_Comment(_("Vacation")); + + if ($tests) { + $test = new Sieve_Test_Allof($tests); + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } else { + $this->_blocks[] = $action[0]; + } + } + + /** + * Adds all blocks necessary for the spam rule. + */ + function _addSpamBlocks() + { + if (!$this->_validRule(Ingo_Storage::ACTION_SPAM)) { + return; + } + + $spam = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_SPAM); + if ($spam === false) { + return; + } + + $this->_blocks[] = new Sieve_Comment(_("Spam Filter")); + + $actions = array(); + $actions[] = new Sieve_Action_Fileinto(array( + 'folder' => $spam->getSpamFolder() + )); + + if ($GLOBALS['conf']['spam']['compare'] == 'numeric') { + $vals = array( + 'headers' => $GLOBALS['conf']['spam']['header'], + 'comparison' => 'ge', + 'value' => $spam->getSpamLevel(), + ); + $test = new Sieve_Test_Relational($vals); + } elseif ($GLOBALS['conf']['spam']['compare'] == 'string') { + $vals = array( + 'headers' => $GLOBALS['conf']['spam']['header'], + 'match-type' => ':contains', + 'strings' => str_repeat($GLOBALS['conf']['spam']['char'], + $spam->getSpamLevel()), + 'comparator' => 'i;ascii-casemap', + ); + $test = new Sieve_Test_Header($vals); + } + + $actions[] = new Sieve_Action_Stop(); + + $if = new Sieve_If($test); + $if->setActions($actions); + $this->_blocks[] = $if; + } + + /** + * Generates the Sieve script to do the filtering specified in + * the rules. + * + * @return string The Sieve script. + */ + function generate() + { + global $ingo_storage; + + $filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); + foreach ($filters->getFilterlist() as $filter) { + /* Check to make sure this is a valid rule and that the rule + is not disabled. */ + if (!$this->_validRule($filter['action']) || + !empty($filter['disable'])) { + continue; + } + + $action = array(); + switch ($filter['action']) { + case Ingo_Storage::ACTION_KEEP: + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags'])); + } + + $action[] = new Sieve_Action_Keep(); + + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags'])); + } + break; + + case Ingo_Storage::ACTION_DISCARD: + $action[] = new Sieve_Action_Discard(); + break; + + case Ingo_Storage::ACTION_MOVE: + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags'])); + } + + $action[] = new Sieve_Action_Fileinto(array('folder' => $filter['action-value'])); + + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags'])); + } + break; + + case Ingo_Storage::ACTION_REJECT: + $action[] = new Sieve_Action_Reject(array('reason' => $filter['action-value'])); + break; + + case Ingo_Storage::ACTION_REDIRECT: + $action[] = new Sieve_Action_Redirect(array('address' => $filter['action-value'])); + break; + + case Ingo_Storage::ACTION_REDIRECTKEEP: + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags'])); + } + + $action[] = new Sieve_Action_Redirect(array('address' => $filter['action-value'])); + $action[] = new Sieve_Action_Keep(); + + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags'])); + } + break; + + case Ingo_Storage::ACTION_MOVEKEEP: + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags'])); + } + + $action[] = new Sieve_Action_Keep(); + $action[] = new Sieve_Action_Fileinto(array('folder' => $filter['action-value'])); + + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags'])); + } + break; + + case Ingo_Storage::ACTION_FLAGONLY: + if (!empty($filter['flags'])) { + $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags'])); + } + break; + + case Ingo_Storage::ACTION_NOTIFY: + $action[] = new Sieve_Action_Notify(array('address' => $filter['action-value'], 'name' => $filter['name'])); + break; + + case Ingo_Storage::ACTION_WHITELIST: + $this->_addWhitelistBlocks(); + continue 2; + + case Ingo_Storage::ACTION_BLACKLIST: + $this->_addBlacklistBlocks(); + continue 2; + + case Ingo_Storage::ACTION_VACATION: + $this->_addVacationBlocks(); + continue 2; + + case Ingo_Storage::ACTION_FORWARD: + $this->_addForwardBlocks(); + continue 2; + + case Ingo_Storage::ACTION_SPAM: + $this->_addSpamBlocks(); + continue 2; + } + + $this->_blocks[] = new Sieve_Comment($filter['name']); + + if ($filter['stop']) { + $action[] = new Sieve_Action_Stop(); + } + + $test = new Sieve_Test(); + if ($filter['combine'] == Ingo_Storage::COMBINE_ANY) { + $test = new Sieve_Test_Anyof(); + } else { + $test = new Sieve_Test_Allof(); + } + + foreach ($filter['conditions'] as $condition) { + $tmp = ''; + switch ($condition['match']) { + case 'equal to': + $tmp = new Sieve_Test_Relational(array('comparison' => 'eq', 'headers' => $condition['field'], 'value' => $condition['value'])); + $test->addTest($tmp); + break; + + case 'not equal': + $tmp = new Sieve_Test_Relational(array('comparison' => 'ne', 'headers' => $condition['field'], 'value' => $condition['value'])); + $test->addTest($tmp); + break; + + case 'less than': + if ($condition['field'] == 'Size') { + /* Message Size Test. */ + $tmp = new Sieve_Test_Size(array('comparison' => ':under', 'size' => $condition['value'])); + } else { + /* Relational Test. */ + $tmp = new Sieve_Test_Relational(array('comparison' => 'lt', 'headers' => $condition['field'], 'value' => $condition['value'])); + } + $test->addTest($tmp); + break; + + case 'less than or equal to': + $tmp = new Sieve_Test_Relational(array('comparison' => 'le', 'headers' => $condition['field'], 'value' => $condition['value'])); + $test->addTest($tmp); + break; + + case 'greater than': + if ($condition['field'] == 'Size') { + /* Message Size Test. */ + $tmp = new Sieve_Test_Size(array('comparison' => ':over', 'size' => $condition['value'])); + } else { + /* Relational Test. */ + $tmp = new Sieve_Test_Relational(array('comparison' => 'gt', 'headers' => $condition['field'], 'value' => $condition['value'])); + } + $test->addTest($tmp); + break; + + case 'greater than or equal to': + $tmp = new Sieve_Test_Relational(array('comparison' => 'ge', 'headers' => $condition['field'], 'value' => $condition['value'])); + $test->addTest($tmp); + break; + + case 'exists': + $tmp = new Sieve_Test_Exists(array('headers' => $condition['field'])); + $test->addTest($tmp); + break; + + case 'not exist': + $tmp = new Sieve_Test_Exists(array('headers' => $condition['field'])); + $test->addTest(new Sieve_Test_Not($tmp)); + break; + + case 'contains': + case 'not contain': + case 'is': + case 'not is': + case 'begins with': + case 'not begins with': + case 'ends with': + case 'not ends with': + case 'regex': + case 'matches': + case 'not matches': + $comparator = (isset($condition['case']) && + $condition['case']) + ? 'i;octet' + : 'i;ascii-casemap'; + $vals = array('headers' => preg_replace('/(.)(? $comparator); + $use_address_test = false; + + if ($condition['match'] != 'regex') { + $condition['value'] = preg_replace('/(.)(?addTest($tmp); + break; + + case 'not contain': + $vals['match-type'] = ':contains'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest(new Sieve_Test_Not($tmp)); + break; + + case 'is': + $vals['match-type'] = ':is'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest($tmp); + break; + + case 'not is': + $vals['match-type'] = ':is'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest(new Sieve_Test_Not($tmp)); + break; + + case 'begins with': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = $v . '*'; + } + $vals['addresses'] = implode("\r\n", $add_arr); + } else { + $vals['addresses'] .= '*'; + } + $tmp = new Sieve_Test_Address($vals); + } else { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = $v . '*'; + } + $vals['strings'] = implode("\r\n", $add_arr); + } else { + $vals['strings'] .= '*'; + } + if ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + } + $test->addTest($tmp); + break; + + case 'not begins with': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = $v . '*'; + } + $vals['addresses'] = implode("\r\n", $add_arr); + } else { + $vals['addresses'] .= '*'; + } + $tmp = new Sieve_Test_Address($vals); + } else { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = $v . '*'; + } + $vals['strings'] = implode("\r\n", $add_arr); + } else { + $vals['strings'] .= '*'; + } + if ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + } + $test->addTest(new Sieve_Test_Not($tmp)); + break; + + case 'ends with': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = '*' . $v; + } + $vals['addresses'] = implode("\r\n", $add_arr); + } else { + $vals['addresses'] = '*' . $vals['addresses']; + } + $tmp = new Sieve_Test_Address($vals); + } else { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = '*' . $v; + } + $vals['strings'] = implode("\r\n", $add_arr); + } else { + $vals['strings'] = '*' . $vals['strings']; + } + if ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + } + $test->addTest($tmp); + break; + + case 'not ends with': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = '*' . $v; + } + $vals['addresses'] = implode("\r\n", $add_arr); + } else { + $vals['addresses'] = '*' . $vals['addresses']; + } + $tmp = new Sieve_Test_Address($vals); + } else { + $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']); + if (count($add_arr) > 1) { + foreach ($add_arr as $k => $v) { + $add_arr[$k] = '*' . $v; + } + $vals['strings'] = implode("\r\n", $add_arr); + } else { + $vals['strings'] = '*' . $vals['strings']; + } + if ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + } + $test->addTest(new Sieve_Test_Not($tmp)); + break; + + case 'regex': + $vals['match-type'] = ':regex'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest($tmp); + break; + + case 'matches': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest($tmp); + break; + + case 'not matches': + $vals['match-type'] = ':matches'; + if ($use_address_test) { + $tmp = new Sieve_Test_Address($vals); + } elseif ($condition['field'] == 'Body') { + $tmp = new Sieve_Test_Body($vals); + } else { + $tmp = new Sieve_Test_Header($vals); + } + $test->addTest(new Sieve_Test_Not($tmp)); + break; + } + } + } + + $if = new Sieve_If($test); + $if->setActions($action); + $this->_blocks[] = $if; + } + + /* Add blocks that have to go to the end. */ + foreach ($this->_endBlocks as $block) { + $this->_blocks[] = $block; + } + + return $this->toCode(); + } + +} + +/** + * The Sieve_If class represents a Sieve If Statement + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_If { + + /** + * The Sieve_Test object for the if test. + * + * @var Sieve_Test + */ + var $_test; + + /** + * A list of Sieve_Action objects that go into the if clause. + * + * @var array + */ + var $_actions = array(); + + /** + * A list of Sieve_Elseif objects that create optional elsif clauses. + * + * @var array + */ + var $_elsifs = array(); + + /** + * A Sieve_Else object that creates an optional else clause. + * + * @var Sieve_Else + */ + var $_else; + + /** + * Constructor. + * + * @params Sieve_Test $test A Sieve_Test object. + */ + function Sieve_If($test = null) + { + if (is_null($test)) { + $this->_test = new Sieve_Test_False(); + } else { + $this->_test = $test; + } + + $this->_actions[] = new Sieve_Action_Keep(); + $this->_else = new Sieve_Else(); + } + + function getTest() + { + return $this->_test; + } + + function setTest($test) + { + $this->_test = $test; + } + + function getActions() + { + return $this->_actions; + } + + function setActions($actions) + { + $this->_actions = $actions; + } + + function getElsifs() + { + return $this->_elsifs; + } + + function setElsifs($elsifs) + { + $this->_elsifs = $elsifs; + } + + function addElsif($elsif) + { + $this->_elsifs[] = $elsif; + } + + function getElse() + { + return $this->_else; + } + + function setElse($else) + { + $this->_else = $else; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'if ' . $this->_test->toCode() . " { \n"; + foreach ($this->_actions as $action) { + $code .= ' ' . $action->toCode() . "\n"; + } + $code .= "} "; + + foreach ($this->_elsifs as $elsif) { + $code .= $elsif->toCode(); + } + + $code .= $this->_else->toCode(); + + return $code . "\n"; + } + + /** + * Checks if all sub-rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + $res = $this->_test->check(); + if ($res !== true) { + return $res; + } + + foreach ($this->_elsifs as $elsif) { + $res = $elsif->check(); + if ($res !== true) { + return $res; + } + } + + $res = $this->_else->check(); + if ($res !== true) { + return $res; + } + + foreach ($this->_actions as $action) { + $res = $action->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + + foreach ($this->_actions as $action) { + $requires = array_merge($requires, $action->requires()); + } + + foreach ($this->_elsifs as $elsif) { + $requires = array_merge($requires, $elsif->requires()); + } + + $requires = array_merge($requires, $this->_test->requires(), $this->_else->requires()); + + return $requires; + } + +} + +/** + * The Sieve_Else class represents a Sieve Else Statement + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Else { + + /** + * A list of Sieve_Action objects that go into the else clause. + * + * @var array + */ + var $_actions = array(); + + /** + * Constructor. + * + * @params Sieve_Action|array $actions A Sieve_Action object or a list of + * Sieve_Action objects. + */ + function Sieve_Else($actions = null) + { + if (is_array($actions)) { + $this->_actions = $actions; + } elseif (!is_null($actions)) { + $this->_actions[] = $actions; + } + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + if (count($this->_actions) == 0) { + return ''; + } + + $code = 'else' . " { \n"; + foreach ($this->_actions as $action) { + $code .= ' ' . $action->toCode() . "\n"; + } + $code .= "} "; + + return $code; + } + + function setActions($actions) + { + $this->_actions = $actions; + } + + function getActions() + { + return $this->_actions; + } + + /** + * Checks if all sub-rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + foreach ($this->_actions as $action) { + $res = $action->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + + foreach ($this->_actions as $action) { + $requires = array_merge($requires, $action->requires()); + } + + return $requires; + } + +} + +/** + * The Sieve_Elsif class represents a Sieve Elsif Statement + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Elsif { + + /** + * The Sieve_Test object for the if test. + * + * @var Sieve_Test + */ + var $_test; + + /** + * A list of Sieve_Action objects that go into the if clause. + * + * @var array + */ + var $_actions = array(); + + /** + * Constructor. + * + * @params Sieve_Test $test A Sieve_Test object. + */ + function Sieve_Elsif($test = null) + { + if (is_null($test)) { + $this->_test = new Sieve_Test_False(); + } else { + $this->_test = $test; + } + $this->_actions[] = new Sieve_Action_Keep(); + } + + function getTest() + { + return $this->_test; + } + + function setTest($test) + { + $this->_test = $test; + } + + function getActions() + { + return $this->_actions; + } + + function setActions($actions) + { + $this->_actions = $actions; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'elsif ' . $this->_test->toCode() . " { \n"; + foreach ($this->_actions as $action) { + $code .= ' ' . $action->toCode() . "\n"; + } + $code .= "} "; + + return $code; + } + + /** + * Checks if all sub-rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + $res = $this->_test->check(); + if ($res !== true) { + return $res; + } + + foreach ($this->_actions as $action) { + $res = $action->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + + foreach ($this->_actions as $action) { + $requires = array_merge($requires, $action->requires()); + } + + $requires = array_merge($requires, $this->_test->requires()); + + return $requires; + } + +} + +/** + * The Sieve_Test class represents a Sieve Test. + * + * A test is a piece of code that evaluates to true or false. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test { + + /** + * Any necessary test parameters. + * + * @var array + */ + var $_vars = array(); + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'toCode() Function Not Implemented in class ' . get_class($this); + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return 'check() Function Not Implemented in class ' . get_class($this); + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array(); + } + +} + +/** + * The Sieve_Test_True class represents a test that always evaluates to true. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_True extends Sieve_Test { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'true'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + +} + +/** + * The Sieve_Test_False class represents a test that always evaluates to + * false. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_False extends Sieve_Test { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'false'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + +} + +/** + * The Sieve_Test_Allof class represents a Allof test structure. + * + * Equivalent to a logical AND of all the tests it contains. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Allof extends Sieve_Test { + + var $_tests = array(); + + /** + * Constructor. + * + * @params Sieve_Test|array $test A Sieve_Test object or a list of + * Sieve_Test objects. + */ + function Sieve_Test_Allof($test = null) + { + if (is_array($test)) { + $this->_tests = $test; + } elseif (!is_null($test)) { + $this->_tests[] = $test; + } + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = ''; + if (count($this->_tests) > 1) { + $testlist = ''; + foreach ($this->_tests as $test) { + $testlist .= (empty($testlist)) ? '' : ', '; + $testlist .= trim($test->toCode()); + } + + $code = "allof ( $testlist )"; + } elseif (count($this->_tests) == 1) { + $code = $this->_tests[0]->toCode(); + } else { + return 'true'; + } + return $code; + } + + /** + * Checks if all sub-rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + foreach ($this->_tests as $test) { + $res = $test->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + function addTest($test) + { + $this->_tests[] = $test; + } + + function getTests() + { + return $this->_tests; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + + foreach ($this->_tests as $test) { + $requires = array_merge($requires, $test->requires()); + } + + return $requires; + } + +} + +/** + * The Sieve_Test_Anyof class represents a Anyof test structure. + * + * Equivalent to a logical OR of all the tests it contains. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Anyof extends Sieve_Test { + + var $_tests = array(); + + /** + * Constructor. + * + * @params Sieve_Test|array $test A Sieve_Test object or a list of + * Sieve_Test objects. + */ + function Sieve_Test_Anyof($test = null) + { + if (is_array($test)) { + $this->_tests = $test; + } elseif (!is_null($test)) { + $this->_tests[] = $test; + } + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $testlist = ''; + if (count($this->_tests) > 1) { + $testlist = ''; + foreach ($this->_tests as $test) { + $testlist .= (empty($testlist)) ? '' : ', '; + $testlist .= trim($test->toCode()); + } + + $code = "anyof ( $testlist )"; + } elseif (count($this->_tests) == 1) { + $code = $this->_tests[0]->toCode(); + } else { + return 'true'; + } + return $code; + } + + function addTest($test) + { + $this->_tests[] = $test; + } + + function getTests() + { + return $this->_tests; + } + + /** + * Checks if all sub-rules are valid. + * + * @return boolean|string True if all rules are valid, an error message + * otherwise. + */ + function check() + { + foreach ($this->_tests as $test) { + $res = $test->check(); + if ($res !== true) { + return $res; + } + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + $requires = array(); + + foreach ($this->_tests as $test) { + $requires = array_merge($requires, $test->requires()); + } + + return $requires; + } + +} + +/** + * The Sieve_Test_Relational class represents a relational test. + * + * @author Todd Merritt + * @since Ingo 1.0 + * @package Ingo + */ +class Sieve_Test_Relational extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Relational($vars = array()) + { + $this->_vars['comparison'] = (isset($vars['comparison'])) ? $vars['comparison'] : ''; + $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : ''; + $this->_vars['value'] = (isset($vars['value'])) ? $vars['value'] : 0; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'header :value "' . + $this->_vars['comparison'] . '" ' . + ':comparator "i;ascii-numeric" '; + + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + $header_count = count($headers); + + if ($header_count > 1) { + $code .= "["; + $headerstr = ''; + + foreach ($headers as $val) { + $headerstr .= (empty($headerstr) ? '"' : ', "') . + Ingo_Script_sieve::escapeString($val) . '"'; + } + + $code .= $headerstr . "] "; + } elseif ($header_count == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString($headers[0]) . '" '; + } + + return $code . '["' . $this->_vars['value'] . '"]'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + return $headers ? true : _("No headers specified"); + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('relational', 'comparator-i;ascii-numeric'); + } + +} + +/** + * The Sieve_Test_Size class represents a message size test. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Size extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Size($vars = array()) + { + $this->_vars['comparison'] = (isset($vars['comparison'])) ? $vars['comparison'] : ''; + $this->_vars['size'] = (isset($vars['size'])) ? $vars['size'] : ''; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'size ' . $this->_vars['comparison'] . ' ' . $this->_vars['size']; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (!(isset($this->_vars['comparison']) && + isset($this->_vars['size']))) { + return false; + } + + return true; + } + +} + +/** + * The Sieve_Test_Not class represents the inverse of a given test. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Not extends Sieve_Test { + + var $_test = array(); + + /** + * Constructor. + * + * @params Sieve_Test $test A Sieve_Test object. + */ + function Sieve_Test_Not($test) + { + $this->_test = $test; + } + + /** + * Checks if the sub-rule is valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return $this->_test->check(); + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'not ' . $this->_test->toCode(); + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return $this->_test->requires(); + } + +} + +/** + * The Sieve_Test_Exists class represents a test for the existsance of one or + * more headers in a message. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Exists extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Exists($vars = array()) + { + $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : ''; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + if (!$headers) { + return _("No headers specified"); + } + + return true; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'exists '; + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + if (count($headers) > 1) { + $code .= "["; + $headerstr = ''; + foreach ($headers as $header) { + $headerstr .= (empty($headerstr) ? '"' : ', "') . + Ingo_Script_sieve::escapeString($header) . '"'; + } + $code .= $headerstr . "] "; + } elseif (count($headers) == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString($headers[0]) . '" '; + } else { + return "**error** No Headers Specified"; + } + + return $code; + } + +} + +/** + * The Sieve_Test_Address class represents a test on parts or all of the + * addresses in the given fields. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Address extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Address($vars) + { + $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : ''; + $this->_vars['comparator'] = (isset($vars['comparator'])) ? $vars['comparator'] : 'i;ascii-casemap'; + $this->_vars['match-type'] = (isset($vars['match-type'])) ? $vars['match-type'] : ':is'; + $this->_vars['address-part'] = (isset($vars['address-part'])) ? $vars['address-part'] : ':all'; + $this->_vars['addresses'] = (isset($vars['addresses'])) ? $vars['addresses'] : ''; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + if (!$headers) { + return false; + } + + $addresses = preg_split('(\r\n|\n|\r)', $this->_vars['addresses']); + if (!$addresses) { + return false; + } + + return true; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'address ' . + $this->_vars['address-part'] . ' ' . + ':comparator "' . $this->_vars['comparator'] . '" ' . + $this->_vars['match-type'] . ' '; + + $headers = preg_split('(\r\n|\n|\r|,)', $this->_vars['headers']); + $headers = array_filter($headers); + if (count($headers) > 1) { + $code .= "["; + $headerstr = ''; + foreach ($headers as $header) { + $header = trim($header); + if (!empty($header)) { + $headerstr .= empty($headerstr) ? '"' : ', "'; + $headerstr .= Ingo_Script_sieve::escapeString($header, $this->_vars['match-type'] == ':regex') . '"'; + } + } + $code .= $headerstr . "] "; + } elseif (count($headers) == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString($headers[0], $this->_vars['match-type'] == ':regex') . '" '; + } else { + return "No Headers Specified"; + } + + $addresses = preg_split('(\r\n|\n|\r)', $this->_vars['addresses']); + $addresses = array_filter($addresses); + if (count($addresses) > 1) { + $code .= "["; + $addressstr = ''; + foreach ($addresses as $addr) { + $addr = trim($addr); + if (!empty($addr)) { + $addressstr .= empty($addressstr) ? '"' : ', "'; + $addressstr .= Ingo_Script_sieve::escapeString($addr, $this->_vars['match-type'] == ':regex') . '"'; + } + } + $code .= $addressstr . "] "; + } elseif (count($addresses) == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString($addresses[0], $this->_vars['match-type'] == ':regex') . '" '; + } else { + return "No Addresses Specified"; + } + + return $code; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + if ($this->_vars['match-type'] == ':regex') { + return array('regex'); + } + return array(); + } + +} + +/** + * The Sieve_Test_Header class represents a test on the contents of one or + * more headers in a message. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Test_Header extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Header($vars = array()) + { + $this->_vars['headers'] = isset($vars['headers']) + ? $vars['headers'] + : 'Subject'; + $this->_vars['comparator'] = isset($vars['comparator']) + ? $vars['comparator'] + : 'i;ascii-casemap'; + $this->_vars['match-type'] = isset($vars['match-type']) + ? $vars['match-type'] + : ':is'; + $this->_vars['strings'] = isset($vars['strings']) + ? $vars['strings'] + : ''; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + $headers = preg_split('((?_vars['headers']); + if (!$headers) { + return false; + } + + $strings = preg_split('((?_vars['strings']); + if (!$strings) { + return false; + } + + return true; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'header ' . + ':comparator "' . $this->_vars['comparator'] . '" ' . + $this->_vars['match-type'] . ' '; + + $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']); + $headers = array_filter($headers); + if (count($headers) > 1) { + $code .= "["; + $headerstr = ''; + foreach ($headers as $header) { + $headerstr .= empty($headerstr) ? '"' : ', "'; + $headerstr .= Ingo_Script_sieve::escapeString($header, $this->_vars['match-type'] == ':regex') . '"'; + } + $code .= $headerstr . "] "; + } elseif (count($headers) == 1) { + $code .= '"' . $headers[0] . '" '; + } else { + return _("No headers specified"); + } + + $strings = preg_split('(\r\n|\n|\r)', $this->_vars['strings']); + $strings = array_filter($strings); + if (count($strings) > 1) { + $code .= "["; + $stringlist = ''; + foreach ($strings as $str) { + $stringlist .= empty($stringlist) ? '"' : ', "'; + $stringlist .= Ingo_Script_sieve::escapeString($str, $this->_vars['match-type'] == ':regex') . '"'; + } + $code .= $stringlist . "] "; + } elseif (count($strings) == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString(reset($strings), $this->_vars['match-type'] == ':regex') . '" '; + } else { + return _("No strings specified"); + } + + return $code; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + if ($this->_vars['match-type'] == ':regex') { + return array('regex'); + } + return array(); + } + +} + +/** + * The Sieve_Test_Body class represents a test on the contents of the body in + * a message. + * + * @author Michael Menge + * @since Ingo 1.2 + * @package Ingo + */ +class Sieve_Test_Body extends Sieve_Test { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Test_Body($vars = array()) + { + $this->_vars['comparator'] = (isset($vars['comparator'])) ? $vars['comparator'] : 'i;ascii-casemap'; + $this->_vars['match-type'] = (isset($vars['match-type'])) ? $vars['match-type'] : ':is'; + $this->_vars['strings'] = (isset($vars['strings'])) ? $vars['strings'] : ''; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + $strings = preg_split('((?_vars['strings']); + if (!$strings) { + return false; + } + + return true; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = 'body ' . + ':comparator "' . $this->_vars['comparator'] . '" ' . + $this->_vars['match-type'] . ' '; + + $strings = preg_split('(\r\n|\n|\r)', $this->_vars['strings']); + $strings = array_filter($strings); + if (count($strings) > 1) { + $code .= "["; + $stringlist = ''; + foreach ($strings as $str) { + $stringlist .= empty($stringlist) ? '"' : ', "'; + $stringlist .= Ingo_Script_sieve::escapeString($str, $this->_vars['match-type'] == ':regex') . '"'; + } + $code .= $stringlist . "] "; + } elseif (count($strings) == 1) { + $code .= '"' . Ingo_Script_sieve::escapeString($strings[0], $this->_vars['match-type'] == ':regex') . '" '; + } else { + return _("No strings specified"); + } + + return $code; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + if ($this->_vars['match-type'] == ':regex') { + return array('regex', 'body'); + } + + return array('body'); + } + +} + +/** + * A Comment. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + * @todo This and Sieve_If should really extends a Sieve_Block eventually. + */ +class Sieve_Comment { + + var $_comment; + + /** + * Constructor. + * + * @params string $comment The comment text. + */ + function Sieve_Comment($comment) + { + $this->_comment = $comment; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $code = ''; + $lines = preg_split('(\r\n|\n|\r)', $this->_comment); + foreach ($lines as $line) { + $line = trim($line); + if (strlen($line)) { + $code .= (empty($code) ? '' : "\n") . '# ' . $line; + } + } + return String::convertCharset($code, NLS::getCharset(), 'UTF-8'); + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array(); + } + +} + +/** + * The Sieve_Action class represents an action in a Sieve script. + * + * An action is anything that has a side effect eg: discard, redirect. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action { + + /** + * Any necessary action parameters. + * + * @var array + */ + var $_vars = array(); + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'toCode() Function Not Implemented in class ' . get_class($this) ; + } + + function toString() + { + return $this->toCode(); + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return 'check() Function Not Implemented in class ' . get_class($this) ; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array(); + } + +} + +/** + * The Sieve_Action_Redirect class represents a redirect action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Redirect extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Redirect($vars = array()) + { + $this->_vars['address'] = (isset($vars['address'])) ? $vars['address'] : ''; + } + + function toCode($depth = 0) + { + return str_repeat(' ', $depth * 4) . 'redirect ' . + '"' . Ingo_Script_sieve::escapeString($this->_vars['address']) . '";'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (empty($this->_vars['address'])) { + return _("Missing address to redirect message to"); + } + + return true; + } + +} + +/** + * The Sieve_Action_Reject class represents a reject action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Reject extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Reject($vars = array()) + { + $this->_vars['reason'] = (isset($vars['reason'])) ? $vars['reason'] : ''; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'reject "' . Ingo_Script_sieve::escapeString($this->_vars['reason']) . '";'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (empty($this->_vars['reason'])) { + return _("Missing reason for reject"); + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('reject'); + } + +} + +/** + * The Sieve_Action_Keep class represents a keep action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Keep extends Sieve_Action { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'keep;'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + +} + +/** + * The Sieve_Action_Discard class represents a discard action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Discard extends Sieve_Action { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'discard;'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + +} + +/** + * The Sieve_Action_Stop class represents a stop action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Stop extends Sieve_Action { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'stop;'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + +} + +/** + * The Sieve_Action_Fileinto class represents a fileinto action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Fileinto extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Fileinto($vars = array()) + { + $this->_vars['folder'] = (isset($vars['folder'])) ? $vars['folder'] : ''; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'fileinto "' . Ingo_Script_sieve::escapeString($this->_vars['folder']) . '";'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (empty($this->_vars['folder'])) { + return _("Inexistant mailbox specified for message delivery."); + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('fileinto'); + } + +} + +/** + * The Sieve_Action_Vacation class represents a vacation action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Vacation extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Vacation($vars = array()) + { + $this->_vars['days'] = isset($vars['days']) ? intval($vars['days']) : ''; + $this->_vars['addresses'] = isset($vars['addresses']) ? $vars['addresses'] : ''; + $this->_vars['subject'] = isset($vars['subject']) ? $vars['subject'] : ''; + $this->_vars['reason'] = isset($vars['reason']) ? $vars['reason'] : ''; + $this->_vars['start'] = isset($vars['start']) ? $vars['start'] : ''; + $this->_vars['start_year'] = isset($vars['start_year']) ? $vars['start_year'] : ''; + $this->_vars['start_month'] = isset($vars['start_month']) ? $vars['start_month'] : ''; + $this->_vars['start_day'] = isset($vars['start_day']) ? $vars['start_day'] : ''; + $this->_vars['end'] = isset($vars['end']) ? $vars['end'] : ''; + $this->_vars['end_year'] = isset($vars['end_year']) ? $vars['end_year'] : ''; + $this->_vars['end_month'] = isset($vars['end_month']) ? $vars['end_month'] : ''; + $this->_vars['end_day'] = isset($vars['end_day']) ? $vars['end_day'] : ''; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + $start_year = $this->_vars['start_year']; + $start_month = $this->_vars['start_month']; + $start_day = $this->_vars['start_day']; + + $end_year = $this->_vars['end_year']; + $end_month = $this->_vars['end_month']; + $end_day = $this->_vars['end_day']; + + $code = ''; + + if (empty($this->_vars['start']) || empty($this->_vars['end'])) { + return $this->_vacationCode(); + } elseif ($end_year > $start_year + 1) { + $code .= $this->_yearCheck($start_year + 1, $end_year - 1) + . $this->_vacationCode() + . "\n}\n" + . $this->_yearCheck($start_year, $start_year); + if ($start_month < 12) { + $code .= $this->_monthCheck($start_month + 1, 12) + . $this->_vacationCode() + . "\n}\n"; + } + $code .= $this->_monthCheck($start_month, $start_month) + . $this->_dayCheck($start_day, 31) + . $this->_vacationCode() + . "\n}\n}\n}\n" + . $this->_yearCheck($end_year, $end_year); + if ($end_month > 1) { + $code .= $this->_monthCheck(1, $end_month - 1) + . $this->_vacationCode() + . "\n}\n"; + } + $code .= $this->_monthCheck($end_month, $end_month) + . $this->_dayCheck(1, $end_day) + . $this->_vacationCode() + . "\n}\n}\n}\n"; + } elseif ($end_year == $start_year + 1) { + $code .= $this->_yearCheck($start_year, $start_year); + if ($start_month < 12) { + $code .= $this->_monthCheck($start_month + 1, 12) + . $this->_vacationCode() + . "\n}\n"; + } + $code .= $this->_monthCheck($start_month, $start_month) + . $this->_dayCheck($start_day, 31) + . $this->_vacationCode() + . "\n}\n}\n}\n" + . $this->_yearCheck($end_year, $end_year); + if ($end_month > 1) { + $code .= $this->_monthCheck(1, $end_month - 1) + . $this->_vacationCode() + . "\n}\n"; + } + $code .= $this->_monthCheck($end_month, $end_month) + . $this->_dayCheck(1, $end_day) + . $this->_vacationCode() + . "\n}\n}\n}\n"; + } elseif ($end_year == $start_year) { + $code .= $this->_yearCheck($start_year, $start_year); + if ($end_month > $start_month) { + if ($end_month > $start_month + 1) { + $code .= $this->_monthCheck($start_month + 1, $end_month - 1) + . $this->_vacationCode() + . "\n}\n"; + } + $code .= $this->_monthCheck($start_month, $start_month) + . $this->_dayCheck($start_day, 31) + . $this->_vacationCode() + . "\n}\n}\n" + . $this->_monthCheck($end_month, $end_month) + . $this->_dayCheck(1, $end_day) + . $this->_vacationCode() + . "\n}\n}\n"; + } elseif ($end_month == $start_month) { + $code .= $this->_monthCheck($start_month, $start_month) + . $this->_dayCheck($start_day, $end_day) + . $this->_vacationCode() + . "\n}\n}\n"; + } + $code .= "}\n"; + } + + return $code; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (empty($this->_vars['reason'])) { + return _("Missing reason in vacation."); + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('vacation', 'regex'); + } + + /** + */ + function _vacationCode() + { + $code = 'vacation :days ' . $this->_vars['days'] . ' '; + $addresses = $this->_vars['addresses']; + $stringlist = ''; + if (count($addresses) > 1) { + foreach ($addresses as $address) { + $address = trim($address); + if (!empty($address)) { + $stringlist .= empty($stringlist) ? '"' : ', "'; + $stringlist .= Ingo_Script_sieve::escapeString($address) . '"'; + } + } + $stringlist = "[" . $stringlist . "] "; + } elseif (count($addresses) == 1) { + $stringlist = '"' . Ingo_Script_sieve::escapeString($addresses[0]) . '" '; + } + + if (!empty($stringlist)) { + $code .= ':addresses ' . $stringlist; + } + + if (!empty($this->_vars['subject'])) { + include_once 'Horde/MIME.php'; + $code .= ':subject "' . MIME::encode(Ingo_Script_sieve::escapeString($this->_vars['subject']), 'UTF-8') . '" '; + } + return $code + . '"' . Ingo_Script_sieve::escapeString($this->_vars['reason']) + . '";'; + } + + /** + */ + function _yearCheck($begin, $end) + { + $code = 'if header :regex "Received" "^.*(' . $begin; + for ($i = $begin + 1; $i <= $end; $i++) { + $code .= '|' . $i; + } + return $code + . ') (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {' + . "\n "; + } + + /** + */ + function _monthCheck($begin, $end) + { + $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); + $code = 'if header :regex "Received" "^.*(' . $months[$begin - 1]; + for ($i = $begin + 1; $i <= $end; $i++) { + $code .= '|' . $months[$i - 1]; + } + return $code + . ') (\\\\(.*\\\\) )?.... (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {' + . "\n "; + } + + /** + */ + function _dayCheck($begin, $end) + { + $code = 'if header :regex "Received" "^.*(' . str_repeat('[0 ]', 2 - strlen($begin)) . $begin; + for ($i = $begin + 1; $i <= $end; $i++) { + $code .= '|' . str_repeat('[0 ]', 2 - strlen($i)) . $i; + } + return $code + . ') (\\\\(.*\\\\) )?... (\\\\(.*\\\\) )?.... (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {' + . "\n "; + } + +} + +/** + * The Sieve_Action_Flag class is the base class for flag actions. + * + * @author Michael Slusarz + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Flag extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Flag($vars = array()) + { + if (isset($vars['flags'])) { + if ($vars['flags'] & Ingo_Storage::FLAG_ANSWERED) { + $this->_vars['flags'][] = '\Answered'; + } + if ($vars['flags'] & Ingo_Storage::FLAG_DELETED) { + $this->_vars['flags'][] = '\Deleted'; + } + if ($vars['flags'] & Ingo_Storage::FLAG_FLAGGED) { + $this->_vars['flags'][] = '\Flagged'; + } + if ($vars['flags'] & Ingo_Storage::FLAG_SEEN) { + $this->_vars['flags'][] = '\Seen'; + } + } else { + $this->_vars['flags'] = ''; + } + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @param string $mode The sieve flag command to use. Either 'removeflag' + * or 'addflag'. + * + * @return string A Sieve script snippet. + */ + function _toCode($mode) + { + $code = ''; + + if (is_array($this->_vars['flags']) && !empty($this->_vars['flags'])) { + $code .= $mode . ' '; + if (count($this->_vars['flags']) > 1) { + $stringlist = ''; + foreach ($this->_vars['flags'] as $flag) { + $flag = trim($flag); + if (!empty($flag)) { + $stringlist .= empty($stringlist) ? '"' : ', "'; + $stringlist .= Ingo_Script_sieve::escapeString($flag) . '"'; + } + } + $stringlist = '[' . $stringlist . ']'; + $code .= $stringlist . ';'; + } else { + $code .= '"' . Ingo_Script_sieve::escapeString($this->_vars['flags'][0]) . '";'; + } + } + return $code; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('imapflags'); + } + +} + +/** + * The Sieve_Action_Addflag class represents an add flag action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Addflag extends Sieve_Action_Flag { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return $this->_toCode('addflag'); + } + +} + +/** + * The Sieve_Action_Removeflag class represents a remove flag action. + * + * @author Mike Cochrane + * @since Ingo 0.1 + * @package Ingo + */ +class Sieve_Action_Removeflag extends Sieve_Action_Flag { + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return $this->_toCode('removeflag'); + } + +} + +/** + * The Sieve_Action_Notify class represents a notify action. + * + * @author Paul Wolstenholme + * @since Ingo 1.1 + * @package Ingo + */ +class Sieve_Action_Notify extends Sieve_Action { + + /** + * Constructor. + * + * @params array $vars Any required parameters. + */ + function Sieve_Action_Notify($vars = array()) + { + $this->_vars['address'] = isset($vars['address']) ? $vars['address'] : ''; + $this->_vars['name'] = isset($vars['name']) ? $vars['name'] : ''; + } + + /** + * Returns a script snippet representing this rule and any sub-rules. + * + * @return string A Sieve script snippet. + */ + function toCode() + { + return 'notify :method "mailto" :options "' . + Ingo_Script_sieve::escapeString($this->_vars['address']) . + '" :message "' . + _("You have received a new message") . "\n" . + _("From:") . " \$from\$ \n" . + _("Subject:") . " \$subject\$ \n" . + _("Rule:") . ' ' . $this->_vars['name'] . '";'; + } + + /** + * Checks if the rule parameters are valid. + * + * @return boolean|string True if this rule is valid, an error message + * otherwise. + */ + function check() + { + if (empty($this->_vars['address'])) { + return _("Missing address to notify"); + } + + return true; + } + + /** + * Returns a list of sieve extensions required for this rule and any + * sub-rules. + * + * @return array A Sieve extension list. + */ + function requires() + { + return array('notify'); + } + +} diff --git a/ingo/lib/Session.php b/ingo/lib/Session.php new file mode 100644 index 000000000..e80b6aa0d --- /dev/null +++ b/ingo/lib/Session.php @@ -0,0 +1,71 @@ + + * @package Ingo + */ +class Ingo_Session +{ + /** + * Create an ingo session. + * This function should only be called once, when the user first uses + * Ingo in a session. + * + * Creates the $ingo session variable with the following entries: + * 'backend' (array) - The backend configuration to use. + * 'change' (integer) - The timestamp of the last time the rules were + * altered. + * 'storage' (array) - Used by Ingo_Storage:: for caching data. + * 'script_categories' (array) - The list of available categories for the + * Ingo_Script driver in use. + * 'script_generate' (boolean) - Is the Ingo_Script::generate() call + * available? + * + * @return boolean True on success, false on failure. + */ + static public function createSession() + { + global $prefs; + + $_SESSION['ingo'] = array( + 'change' => 0, + 'storage' => array(), + /* Get the backend. */ + 'backend' => Ingo::getBackend()); + + /* Determine if the Ingo_Script:: generate() method is available. */ + $ingo_script = Ingo::loadIngoScript(); + $_SESSION['ingo']['script_generate'] = $ingo_script->generateAvailable(); + + /* Disable categories as specified in preferences */ + $disabled = array(); + if ($prefs->isLocked('blacklist')) { + $disabled[] = INGO_STORAGE_ACTION_BLACKLIST; + } + if ($prefs->isLocked('whitelist')) { + $disabled[] = INGO_STORAGE_ACTION_WHITELIST; + } + if ($prefs->isLocked('vacation')) { + $disabled[] = INGO_STORAGE_ACTION_VACATION; + } + if ($prefs->isLocked('forward')) { + $disabled[] = INGO_STORAGE_ACTION_FORWARD; + } + if ($prefs->isLocked('spam')) { + $disabled[] = INGO_STORAGE_ACTION_SPAM; + } + + /* Set the list of categories this driver supports. */ + $_SESSION['ingo']['script_categories'] = + array_merge($ingo_script->availableActions(), + array_diff($ingo_script->availableCategories(), + $disabled)); + } + +} diff --git a/ingo/lib/Storage.php b/ingo/lib/Storage.php new file mode 100644 index 000000000..db996c965 --- /dev/null +++ b/ingo/lib/Storage.php @@ -0,0 +1,1029 @@ + + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Storage +{ + /** + * Ingo_Storage:: 'combine' constants + */ + const COMBINE_ALL = 1; + const COMBINE_ANY = 2; + + /** + * Ingo_Storage:: 'action' constants + */ + const ACTION_FILTERS = 0; + const ACTION_KEEP = 1; + const ACTION_MOVE = 2; + const ACTION_DISCARD = 3; + const ACTION_REDIRECT = 4; + const ACTION_REDIRECTKEEP = 5; + const ACTION_REJECT = 6; + const ACTION_BLACKLIST = 7; + const ACTION_VACATION = 8; + const ACTION_WHITELIST = 9; + const ACTION_FORWARD = 10; + const ACTION_MOVEKEEP = 11; + const ACTION_FLAGONLY = 12; + const ACTION_NOTIFY = 13; + const ACTION_SPAM = 14; + + /** + * Ingo_Storage:: 'flags' constants + */ + const FLAG_ANSWERED = 1; + const FLAG_DELETED = 2; + const FLAG_FLAGGED = 4; + const FLAG_SEEN = 8; + + /** + * Ingo_Storage:: 'type' constants. + */ + const TYPE_HEADER = 1; + const TYPE_SIZE = 2; + const TYPE_BODY = 3; + + /** + * Driver specific parameters. + * + * @var array + */ + protected $_params = array(); + + /** + * Cached rule objects. + * + * @var array + */ + protected $_cache = array(); + + /** + * Attempts to return a concrete Ingo_Storage instance based on $driver. + * + * @param string $driver The type of concrete Ingo_Storage subclass to + * return. This is based on the storage driver + * ($driver). The code is dynamically included. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return mixed The newly created concrete Ingo_Storage instance, or + * false on an error. + */ + static public function factory($driver = null, $params = null) + { + if (is_null($driver)) { + $driver = $GLOBALS['conf']['storage']['driver']; + } + + $driver = basename($driver); + + if (is_null($params)) { + $params = Horde::getDriverConfig('storage', $driver); + } + + $class = 'Ingo_Storage_' . $driver; + return class_exists($class) + ? new $class($params) + : false; + } + + /** + * Destructor. + */ + protected function __destruct() + { + $cache = &Horde_SessionObjects::singleton(); + + /* Store the current objects. */ + foreach ($this->_cache as $key => $val) { + if (!$val['mod'] && isset($_SESSION['ingo']['storage'][$key])) { + continue; + } + if (isset($_SESSION['ingo']['storage'][$key])) { + $cache->setPruneFlag($_SESSION['ingo']['storage'][$key], true); + } + $_SESSION['ingo']['storage'][$key] = $cache->storeOid($val['ob'], false); + } + } + + /** + * Retrieves the specified data. + * + * @param integer $field The field name of the desired data + * (INGO_STORAGE_ACTION_* constants). + * @param boolean $cache Use the cached object? + * @param boolean $readonly Whether to disable any write operations. + * + * @return Ingo_Storage_rule|Ingo_Storage_filters The specified object. + */ + public function retrieve($field, $cache = true, $readonly = false) + { + /* Don't cache if using shares. */ + if ($cache && empty($GLOBALS['ingo_shares'])) { + if (!isset($this->_cache[$field])) { + $this->_cache[$field] = array('mod' => false); + if (isset($_SESSION['ingo']['storage'][$field])) { + $cacheSess = &Horde_SessionObjects::singleton(); + $this->_cache[$field]['ob'] = $cacheSess->query($_SESSION['ingo']['storage'][$field]); + } else { + $this->_cache[$field]['ob'] = &$this->_retrieve($field, $readonly); + } + } + $ob = &$this->_cache[$field]['ob']; + } else { + $ob = &$this->_retrieve($field, $readonly); + } + + return $ob; + } + + /** + * Retrieves the specified data from the storage backend. + * + * @abstract + * + * @param integer $field The field name of the desired data. + * See lib/Storage.php for the available fields. + * @param boolean $readonly Whether to disable any write operations. + * + * @return Ingo_Storage_rule|Ingo_Storage_filters The specified data. + */ + protected function _retrieve($field, $readonly = false) + { + return false; + } + + /** + * Stores the specified data. + * + * @param Ingo_Storage_rule|Ingo_Storage_filters $ob The object to store. + * @param boolean $cache Cache the object? + * + * @return boolean True on success. + */ + public function store(&$ob, $cache = true) + { + $type = $ob->obType(); + if (in_array($type, array(self::ACTION_BLACKLIST, + self::ACTION_VACATION, + self::ACTION_WHITELIST, + self::ACTION_FORWARD, + self::ACTION_SPAM))) { + $filters = $this->retrieve(self::ACTION_FILTERS); + if ($filters->findRuleId($type) === null) { + switch ($type) { + case self::ACTION_BLACKLIST: + $name = 'Blacklist'; + break; + + case self::ACTION_VACATION: + $name = 'Vacation'; + break; + + case self::ACTION_WHITELIST: + $name = 'Whitelist'; + break; + + case self::ACTION_FORWARD: + $name = 'Forward'; + break; + + case self::ACTION_SPAM: + $name = 'Spam Filter'; + break; + } + $filters->addRule(array('action' => $type, 'name' => $name)); + $result = $this->store($filters, $cache); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + } + + $result = $this->_store($ob); + if ($cache) { + $this->_cache[$ob->obType()] = array('ob' => $ob, 'mod' => true); + } + + return $result; + } + + /** + * Stores the specified data in the storage backend. + * + * @abstract + * + * @param Ingo_Storage_rule|Ingo_Storage_filters $ob The object to store. + * + * @return boolean True on success. + */ + protected function _store(&$ob) + { + return false; + } + + /** + * Returns information on a given action constant. + * + * @param integer $action The INGO_STORAGE_ACTION_* value. + * + * @return stdClass Object with the following values: + *
+     * 'flags' => (boolean) Does this action allow flags to be set?
+     * 'label' => (string) The label for this action.
+     * 'type'  => (string) Either 'folder', 'text', or empty.
+     * 
+ */ + public function getActionInfo($action) + { + $ob = &new stdClass; + $ob->flags = false; + $ob->type = 'text'; + + switch ($action) { + case self::ACTION_KEEP: + $ob->label = _("Deliver into my Inbox"); + $ob->type = false; + $ob->flags = true; + break; + + case self::ACTION_MOVE: + $ob->label = _("Deliver to folder..."); + $ob->type = 'folder'; + $ob->flags = true; + break; + + case self::ACTION_DISCARD: + $ob->label = _("Delete message completely"); + $ob->type = false; + break; + + case self::ACTION_REDIRECT: + $ob->label = _("Redirect to..."); + break; + + case self::ACTION_REDIRECTKEEP: + $ob->label = _("Deliver into my Inbox and redirect to..."); + $ob->flags = true; + break; + + case self::ACTION_MOVEKEEP: + $ob->label = _("Deliver into my Inbox and copy to..."); + $ob->type = 'folder'; + $ob->flags = true; + break; + + case self::ACTION_REJECT: + $ob->label = _("Reject with reason..."); + break; + + case self::ACTION_FLAGONLY: + $ob->label = _("Only flag the message"); + $ob->type = false; + $ob->flags = true; + break; + + case self::ACTION_NOTIFY: + $ob->label = _("Notify email address..."); + break; + } + + return $ob; + } + + /** + * Returns information on a given test string. + * + * @param string $action The test string. + * + * @return stdClass Object with the following values: + *
+     * 'label' => (string) The label for this action.
+     * 'type'  => (string) Either 'int', 'none', or 'text'.
+     * 
+ */ + public function getTestInfo($test) + { + /* Mapping of gettext strings -> labels. */ + $labels = array( + 'contains' => _("Contains"), + 'not contain' => _("Doesn't contain"), + 'is' => _("Is"), + 'not is' => _("Isn't"), + 'begins with' => _("Begins with"), + 'not begins with' => _("Doesn't begin with"), + 'ends with' => _("Ends with"), + 'not ends with' => _("Doesn't end with"), + 'exists' => _("Exists"), + 'not exist' => _("Doesn't exist"), + 'regex' => _("Regular expression"), + 'matches' => _("Matches (with placeholders)"), + 'not matches' => _("Doesn't match (with placeholders)"), + 'less than' => _("Less than"), + 'less than or equal to' => _("Less than or equal to"), + 'greater than' => _("Greater than"), + 'greater than or equal to' => _("Greater than or equal to"), + 'equal' => _("Equal to"), + 'not equal' => _("Not equal to") + ); + + /* The type of tests available. */ + $types = array( + 'int' => array( + 'less than', 'less than or equal to', 'greater than', + 'greater than or equal to', 'equal', 'not equal' + ), + 'none' => array( + 'exists', 'not exist' + ), + 'text' => array( + 'contains', 'not contain', 'is', 'not is', 'begins with', + 'not begins with', 'ends with', 'not ends with', 'regex', + 'matches', 'not matches' + ) + ); + + /* Create the information object. */ + $ob = &new stdClass; + $ob->label = $labels[$test]; + foreach ($types as $key => $val) { + if (in_array($test, $val)) { + $ob->type = $key; + break; + } + } + + return $ob; + } + +} + +/** + * Ingo_Storage_rule:: is the base class for the various action objects + * used by Ingo_Storage. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_rule +{ + /** + * The object type. + * + * @var integer + */ + protected $_obtype; + + /** + * Whether the rule has been saved (if being saved separately). + * + * @var boolean + */ + protected $_saved = false; + + /** + * Returns the object rule type. + * + * @return integer The object rule type. + */ + public function obType() + { + return $this->_obtype; + } + + /** + * Marks the rule as saved or unsaved. + * + * @param boolean $data Whether the rule has been saved. + */ + public function setSaved($data) + { + $this->_saved = $data; + } + + /** + * Returns whether the rule has been saved. + * + * @return boolean True if the rule has been saved. + */ + public function isSaved() + { + return $this->_saved; + } + + /** + * Function to manage an internal address list. + * + * @param mixed $data The incoming data (array or string). + * @param boolean $sort Sort the list? + * + * @return array The address list. + */ + protected function _addressList($data, $sort) + { + $output = array(); + + if (is_array($data)) { + $output = $data; + } else { + $data = trim($data); + $output = (empty($data)) ? array() : preg_split("/\s+/", $data); + } + + if ($sort) { + $output = Horde_Array::prepareAddressList($output); + } + + return $output; + } + +} + +/** + * Ingo_Storage_blacklist is the object used to hold blacklist rule + * information. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_blacklist extends Ingo_Storage_rule +{ + protected $_addr = array(); + protected $_folder = ''; + protected $_obtype = Ingo_Storage::ACTION_BLACKLIST; + + /** + * Sets the list of blacklisted addresses. + * + * @param mixed $data The list of addresses (array or string). + * @param boolean $sort Sort the list? + * + * @return mixed PEAR_Error on error, true on success. + */ + public function setBlacklist($data, $sort = true) + { + $addr = &$this->_addressList($data, $sort); + if (!empty($GLOBALS['conf']['storage']['maxblacklist'])) { + $addr_count = count($addr); + if ($addr_count > $GLOBALS['conf']['storage']['maxblacklist']) { + return PEAR::raiseError(sprintf(_("Maximum number of blacklisted addresses exceeded (Total addresses: %s, Maximum addresses: %s). Could not add new addresses to blacklist."), $addr_count, $GLOBALS['conf']['storage']['maxblacklist']), 'horde.error'); + } + } + + $this->_addr = $addr; + return true; + } + + public function setBlacklistFolder($data) + { + $this->_folder = $data; + } + + public function getBlacklist() + { + return array_filter($this->_addr, array('Ingo', '_filterEmptyAddress')); + } + + public function getBlacklistFolder() + { + return $this->_folder; + } + +} + +/** + * Ingo_Storage_whitelist is the object used to hold whitelist rule + * information. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_whitelist extends Ingo_Storage_rule +{ + protected $_addr = array(); + protected $_obtype = Ingo_Storage::ACTION_WHITELIST; + + /** + * Sets the list of whitelisted addresses. + * + * @param mixed $data The list of addresses (array or string). + * @param boolean $sort Sort the list? + * + * @return mixed PEAR_Error on error, true on success. + */ + public function setWhitelist($data, $sort = true) + { + $addr = &$this->_addressList($data, $sort); + $addr = array_filter($addr, array('Ingo', '_filterEmptyAddress')); + if (!empty($GLOBALS['conf']['storage']['maxwhitelist'])) { + $addr_count = count($addr); + if ($addr_count > $GLOBALS['conf']['storage']['maxwhitelist']) { + return PEAR::raiseError(sprintf(_("Maximum number of whitelisted addresses exceeded (Total addresses: %s, Maximum addresses: %s). Could not add new addresses to whitelist."), $addr_count, $GLOBALS['conf']['storage']['maxwhitelist']), 'horde.error'); + } + } + + $this->_addr = $addr; + return true; + } + + public function getWhitelist() + { + return array_filter($this->_addr, array('Ingo', '_filterEmptyAddress')); + } + +} + +/** + * Ingo_Storage_forward is the object used to hold mail forwarding rule + * information. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_forward extends Ingo_Storage_rule +{ + protected $_addr = array(); + protected $_keep = true; + protected $_obtype = Ingo_Storage::ACTION_FORWARD; + + public function setForwardAddresses($data, $sort = true) + { + $this->_addr = &$this->_addressList($data, $sort); + } + + public function setForwardKeep($data) + { + $this->_keep = $data; + } + + public function getForwardAddresses() + { + if (is_array($this->_addr)) { + foreach ($this->_addr as $key => $val) { + if (empty($val)) { + unset($this->_addr[$key]); + } + } + } + return $this->_addr; + } + + public function getForwardKeep() + { + return $this->_keep; + } + +} + +/** + * Ingo_Storage_vacation is the object used to hold vacation rule + * information. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_vacation extends Ingo_Storage_rule +{ + protected $_addr = array(); + protected $_days = 7; + protected $_excludes = array(); + protected $_ignorelist = true; + protected $_reason = ''; + protected $_subject = ''; + protected $_start; + protected $_end; + protected $_obtype = Ingo_Storage::ACTION_VACATION; + + public function setVacationAddresses($data, $sort = true) + { + $this->_addr = &$this->_addressList($data, $sort); + } + + public function setVacationDays($data) + { + $this->_days = $data; + } + + public function setVacationExcludes($data, $sort = true) + { + $this->_excludes = &$this->_addressList($data, $sort); + } + + public function setVacationIgnorelist($data) + { + $this->_ignorelist = $data; + } + + public function setVacationReason($data) + { + $this->_reason = $data; + } + + public function setVacationSubject($data) + { + $this->_subject = $data; + } + + public function setVacationStart($data) + { + $this->_start = $data; + } + + public function setVacationEnd($data) + { + $this->_end = $data; + } + + public function getVacationAddresses() + { + if (empty($GLOBALS['conf']['hooks']['vacation_addresses'])) { + return $this->_addr; + } + + $addresses = Horde::callHook('_ingo_hook_vacation_addresses', array(Ingo::getUser()), 'ingo'); + if (is_a($addresses, 'PEAR_Error')) { + $addresses = array(); + } + return $addresses; + } + + public function getVacationDays() + { + return $this->_days; + } + + public function getVacationExcludes() + { + return $this->_excludes; + } + + public function getVacationIgnorelist() + { + return $this->_ignorelist; + } + + public function getVacationReason() + { + return $this->_reason; + } + + public function getVacationSubject() + { + return $this->_subject; + } + + public function getVacationStart() + { + return $this->_start; + } + + public function getVacationStartYear() + { + return date('Y', $this->_start); + } + + public function getVacationStartMonth() + { + return date('n', $this->_start); + } + + public function getVacationStartDay() + { + return date('j', $this->_start); + } + + public function getVacationEnd() + { + return $this->_end; + } + + public function getVacationEndYear() + { + return date('Y', $this->_end); + } + + public function getVacationEndMonth() + { + return date('n', $this->_end); + } + + public function getVacationEndDay() + { + return date('j', $this->_end); + } + +} + +/** + * Ingo_Storage_spam is an object used to hold default spam-rule filtering + * information. + * + * @author Jason M. Felice + * @package Ingo + */ +class Ingo_Storage_spam extends Ingo_Storage_rule +{ + + /** + * The object type. + * + * @var integer + */ + protected $_obtype = Ingo_Storage::ACTION_SPAM; + + protected $_folder = null; + protected $_level = 5; + + public function __construct() + { + // Attempt to get the default Spam folder from imp + if (in_array('imp', $GLOBALS['registry']->listApps())) { + $prefs = Prefs::factory($GLOBALS['conf']['prefs']['driver'], + 'imp', Ingo::getUser(), '', null, false); + $prefs->retrieve(); + + $folder = $prefs->getValue('spam_folder', false); + if ($folder !== false) { + $this->_folder = $folder; + } + } + } + + public function setSpamFolder($folder) + { + $this->_folder = $folder; + } + + public function setSpamLevel($level) + { + $this->_level = $level; + } + + public function getSpamFolder() + { + return $this->_folder; + } + + public function getSpamLevel() + { + return $this->_level; + } + +} + +/** + * Ingo_Storage_filters is the object used to hold user-defined filtering rule + * information. + * + * @author Michael Slusarz + * @package Ingo + */ +class Ingo_Storage_filters +{ + /** + * The filter list. + * + * @var array + */ + protected $_filters = array(); + + /** + * The object type. + * + * @var integer + */ + protected $_obtype = Ingo_Storage::ACTION_FILTERS; + + /** + * Returns the object rule type. + * + * @return integer The object rule type. + */ + public function obType() + { + return $this->_obtype; + } + + /** + * Propagates the filter list with data. + * + * @param array $data A list of rule hashes. + */ + public function setFilterlist($data) + { + $this->_filters = $data; + } + + /** + * Returns the filter list. + * + * @return array The list of rule hashes. + */ + public function getFilterlist() + { + return $this->_filters; + } + + /** + * Returns a single rule hash. + * + * @param integer $id A rule number. + * + * @return array The requested rule hash. + */ + public function getRule($id) + { + return $this->_filters[$id]; + } + + /** + * Returns a rule hash with default value used when creating new rules. + * + * @return array A rule hash. + */ + public function getDefaultRule() + { + return array( + 'name' => _("New Rule"), + 'combine' => INGO_STORAGE_COMBINE_ALL, + 'conditions' => array(), + 'action' => INGO_STORAGE_ACTION_KEEP, + 'action-value' => '', + 'stop' => true, + 'flags' => 0, + 'disable' => false + ); + } + + /** + * Searches for the first rule of a certain action type and returns its + * number. + * + * @param integer $action The field type of the searched rule + * (INGO_STORAGE_ACTION_* constants). + * + * @return integer The number of the first matching rule or null. + */ + public function findRuleId($action) + { + foreach ($this->_filters as $id => $rule) { + if ($rule['action'] == $action) { + return $id; + } + } + } + + /** + * Searches for and returns the first rule of a certain action type. + * + * @param integer $action The field type of the searched rule + * (INGO_STORAGE_ACTION_* constants). + * + * @return array The first matching rule hash or null. + */ + public function findRule($action) + { + $id = $this->findRuleId($action); + if ($id !== null) { + return $this->getRule($id); + } + } + + /** + * Adds a rule hash to the filters list. + * + * @param array $rule A rule hash. + * @param boolean $default If true merge the rule hash with default rule + * values. + */ + public function addRule($rule, $default = true) + { + if ($default) { + $this->_filters[] = array_merge($this->getDefaultRule(), $rule); + } else { + $this->_filters[] = $rule; + } + } + + /** + * Updates an existing rule with a rule hash. + * + * @param array $rule A rule hash + * @param integer $id A rule number + */ + public function updateRule($rule, $id) + { + $this->_filters[$id] = $rule; + } + + /** + * Deletes a rule from the filters list. + * + * @param integer $id Number of the rule to delete. + * + * @return boolean True if the rule has been found and deleted. + */ + public function deleteRule($id) + { + if (isset($this->_filters[$id])) { + unset($this->_filters[$id]); + $this->_filters = array_values($this->_filters); + return true; + } + + return false; + } + + /** + * Creates a copy of an existing rule. + * + * The created copy is added to the filters list right after the original + * rule. + * + * @param integer $id Number of the rule to copy. + * + * @return boolean True if the rule has been found and copied. + */ + public function copyRule($id) + { + if (isset($this->_filters[$id])) { + $newrule = $this->_filters[$id]; + $newrule['name'] = sprintf(_("Copy of %s"), $this->_filters[$id]['name']); + $this->_filters = array_merge(array_slice($this->_filters, 0, $id + 1), array($newrule), array_slice($this->_filters, $id + 1)); + return true; + } + + return false; + } + + /** + * Moves a rule up in the filters list. + * + * @param integer $id Number of the rule to move. + * @param integer $steps Number of positions to move the rule up. + */ + public function ruleUp($id, $steps = 1) + { + for ($i = 0; $i < $steps && $id > 0;) { + $temp = $this->_filters[$id - 1]; + $this->_filters[$id - 1] = $this->_filters[$id]; + $this->_filters[$id] = $temp; + /* Continue to move up until we swap with a viewable category. */ + if (in_array($temp['action'], $_SESSION['ingo']['script_categories'])) { + $i++; + } + $id--; + } + } + + /** + * Moves a rule down in the filters list. + * + * @param integer $id Number of the rule to move. + * @param integer $steps Number of positions to move the rule down. + */ + public function ruleDown($id, $steps = 1) + { + $rulecount = count($this->_filters) - 1; + for ($i = 0; $i < $steps && $id < $rulecount;) { + $temp = $this->_filters[$id + 1]; + $this->_filters[$id + 1] = $this->_filters[$id]; + $this->_filters[$id] = $temp; + /* Continue to move down until we swap with a viewable + category. */ + if (in_array($temp['action'], $_SESSION['ingo']['script_categories'])) { + $i++; + } + $id++; + } + } + + /** + * Disables a rule. + * + * @param integer $id Number of the rule to disable. + */ + public function ruleDisable($id) + { + $this->_filters[$id]['disable'] = true; + } + + /** + * Enables a rule. + * + * @param integer $id Number of the rule to enable. + */ + public function ruleEnable($id) + { + $this->_filters[$id]['disable'] = false; + } + +} diff --git a/ingo/lib/Storage/mock.php b/ingo/lib/Storage/mock.php new file mode 100644 index 000000000..10a540927 --- /dev/null +++ b/ingo/lib/Storage/mock.php @@ -0,0 +1,55 @@ + + * @package Ingo + */ + +class Ingo_Storage_mock extends Ingo_Storage +{ + protected $_data = array(); + + protected function &_retrieve($field) + { + if (empty($this->_data[$field])) { + switch ($field) { + case self::ACTION_BLACKLIST: + return new Ingo_Storage_blacklist(); + + case self::ACTION_FILTERS: + $ob = &new Ingo_Storage_filters(); + include INGO_BASE . '/config/prefs.php.dist'; + $ob->setFilterList(unserialize($_prefs['rules']['value'])); + return $ob; + + case self::ACTION_FORWARD: + return new Ingo_Storage_forward(); + + case self::ACTION_VACATION: + return new Ingo_Storage_vacation(); + + case self::ACTION_WHITELIST: + return new Ingo_Storage_whitelist(); + + case self::ACTION_SPAM: + return new Ingo_Storage_spam(); + + default: + return false; + } + } + + return $this->_data[$field]; + } + + protected function _store(&$ob) + { + $this->_data[$ob->obType()] = $ob; + } + +} diff --git a/ingo/lib/Storage/prefs.php b/ingo/lib/Storage/prefs.php new file mode 100644 index 000000000..4054717d6 --- /dev/null +++ b/ingo/lib/Storage/prefs.php @@ -0,0 +1,183 @@ + + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Storage_prefs extends Ingo_Storage +{ + /** + * Constructor. + * + * @param array $params Additional parameters for the subclass. + */ + public function __construct($params = array()) + { + $this->_params = $params; + } + + /** + * Retrieves the specified data from the storage backend. + * + * @param integer $field The field name of the desired data. + * See lib/Storage.php for the available fields. + * @param boolean $readonly Whether to disable any write operations. + * + * @return Ingo_Storage_rule|Ingo_Storage_filters The specified data. + */ + protected function _retrieve($field, $readonly = false) + { + $prefs = &Prefs::singleton($GLOBALS['conf']['prefs']['driver'], + $GLOBALS['registry']->getApp(), + Ingo::getUser(), '', null, false); + $prefs->retrieve(); + + switch ($field) { + case self::ACTION_BLACKLIST: + $ob = new Ingo_Storage_blacklist(); + $data = @unserialize($prefs->getValue('blacklist')); + if ($data) { + $ob->setBlacklist($data['a'], false); + $ob->setBlacklistFolder($data['f']); + } + break; + + case self::ACTION_FILTERS: + $ob = new Ingo_Storage_filters(); + $data = @unserialize($prefs->getValue('rules', false)); + if ($data === false) { + /* Convert rules from the old format. */ + $data = @unserialize($prefs->getValue('rules')); + } else { + $data = String::convertCharset($data, $prefs->getCharset(), NLS::getCharset()); + } + if ($data) { + $ob->setFilterlist($data); + } + break; + + case self::ACTION_FORWARD: + $ob = new Ingo_Storage_forward(); + $data = @unserialize($prefs->getValue('forward')); + if ($data) { + $ob->setForwardAddresses($data['a'], false); + $ob->setForwardKeep($data['k']); + } + break; + + case self::ACTION_VACATION: + $ob = new Ingo_Storage_vacation(); + $data = @unserialize($prefs->getValue('vacation', false)); + if ($data === false) { + /* Convert vacation from the old format. */ + $data = unserialize($prefs->getValue('vacation')); + } elseif (is_array($data)) { + $data = $prefs->convertFromDriver($data, NLS::getCharset()); + } + if ($data) { + $ob->setVacationAddresses($data['addresses'], false); + $ob->setVacationDays($data['days']); + $ob->setVacationExcludes($data['excludes'], false); + $ob->setVacationIgnorelist($data['ignorelist']); + $ob->setVacationReason($data['reason']); + $ob->setVacationSubject($data['subject']); + if (isset($data['start'])) { + $ob->setVacationStart($data['start']); + } + if (isset($data['end'])) { + $ob->setVacationEnd($data['end']); + } + } + break; + + case self::ACTION_WHITELIST: + $ob = new Ingo_Storage_whitelist(); + $data = @unserialize($prefs->getValue('whitelist')); + if ($data) { + $ob->setWhitelist($data, false); + } + break; + + case self::ACTION_SPAM: + $ob = new Ingo_Storage_spam(); + $data = @unserialize($prefs->getValue('spam')); + if ($data) { + $ob->setSpamFolder($data['folder']); + $ob->setSpamLevel($data['level']); + } + break; + + default: + $ob = false; + break; + } + + return $ob; + } + + /** + * Stores the specified data in the storage backend. + * + * @param Ingo_Storage_rule|Ingo_Storage_filters $ob The object to store. + * + * @return boolean True on success. + */ + protected function _store($ob) + { + $prefs = &Prefs::singleton($GLOBALS['conf']['prefs']['driver'], + $GLOBALS['registry']->getApp(), + Ingo::getUser(), '', null, false); + $prefs->retrieve(); + + switch ($ob->obType()) { + case self::ACTION_BLACKLIST: + $data = array( + 'a' => $ob->getBlacklist(), + 'f' => $ob->getBlacklistFolder(), + ); + return $prefs->setValue('blacklist', serialize($data)); + + case self::ACTION_FILTERS: + return $prefs->setValue('rules', serialize(String::convertCharset($ob->getFilterlist(), NLS::getCharset(), $prefs->getCharset())), false); + + case self::ACTION_FORWARD: + $data = array( + 'a' => $ob->getForwardAddresses(), + 'k' => $ob->getForwardKeep(), + ); + return $prefs->setValue('forward', serialize($data)); + + case self::ACTION_VACATION: + $data = array( + 'addresses' => $ob->getVacationAddresses(), + 'days' => $ob->getVacationDays(), + 'excludes' => $ob->getVacationExcludes(), + 'ignorelist' => $ob->getVacationIgnorelist(), + 'reason' => $ob->getVacationReason(), + 'subject' => $ob->getVacationSubject(), + 'start' => $ob->getVacationStart(), + 'end' => $ob->getVacationEnd(), + ); + return $prefs->setValue('vacation', serialize($prefs->convertToDriver($data, NLS::getCharset())), false); + + case self::ACTION_WHITELIST: + return $prefs->setValue('whitelist', serialize($ob->getWhitelist())); + + case self::ACTION_SPAM: + $data = array( + 'folder' => $ob->getSpamFolder(), + 'level' => $ob->getSpamLevel(), + ); + return $prefs->setValue('spam', serialize($data)); + } + + return false; + } + +} diff --git a/ingo/lib/Storage/sql.php b/ingo/lib/Storage/sql.php new file mode 100644 index 000000000..b6d6ed77d --- /dev/null +++ b/ingo/lib/Storage/sql.php @@ -0,0 +1,718 @@ + + * 'phptype' - The database type (e.g. 'pgsql', 'mysql', etc.). + * 'charset' - The database's internal charset. + * + * Required by some database implementations:
+ *   'database' - The name of the database.
+ *   'hostspec' - The hostname of the database server.
+ *   'protocol' - The communication protocol ('tcp', 'unix', etc.).
+ *   'username' - The username with which to connect to the database.
+ *   'password' - The password associated with 'username'.
+ *   'options'  - Additional options to pass to the database.
+ *   'tty'      - The TTY on which to connect to the database.
+ *   'port'     - The port on which to connect to the database.
+ * + * The table structure can be created by the scripts/drivers/sql/ingo.sql + * script. + * + * See the enclosed file LICENSE for license information (ASL). If you + * did not receive this file, see http://www.horde.org/licenses/asl.php. + * + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Storage_sql extends Ingo_Storage +{ + /** + * Handle for the current database connection. + * + * @var DB + */ + protected $_db; + + /** + * Handle for the current database connection, used for writing. + * + * Defaults to the same handle as $_db if a separate write database is not + * required. + * + * @var DB + */ + protected $_write_db; + + /** + * Boolean indicating whether or not we're connected to the SQL server. + * + * @var boolean + */ + protected $_connected = false; + + /** + * Constructor. + * + * @param array $params Additional parameters for the subclass. + */ + public function __construct($params = array()) + { + $this->_params = $params; + + Horde::assertDriverConfig($this->_params, 'storage', + array('phptype', 'charset')); + + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + $this->_params['table_rules'] = 'ingo_rules'; + $this->_params['table_lists'] = 'ingo_lists'; + $this->_params['table_vacations'] = 'ingo_vacations'; + $this->_params['table_forwards'] = 'ingo_forwards'; + $this->_params['table_spam'] = 'ingo_spam'; + + /* Connect to the SQL server using the supplied parameters. */ + $this->_write_db = &DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']), + 'ssl' => !empty($this->_params['ssl']))); + if (is_a($this->_write_db, 'PEAR_Error')) { + Horde::fatal($this->_write_db, __FILE__, __LINE__); + } + /* Set DB portability options. */ + switch ($this->_write_db->phptype) { + case 'mssql': + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + + /* Check if we need to set up the read DB connection seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = &DB::connect($params, + array('persistent' => !empty($params['persistent']), + 'ssl' => !empty($params['ssl']))); + if (is_a($this->_db, 'PEAR_Error')) { + Horde::fatal($this->_db, __FILE__, __LINE__); + } + + switch ($this->_db->phptype) { + case 'mssql': + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + } else { + /* Default to the same DB handle for the writer too. */ + $this->_db =& $this->_write_db; + } + } + + /** + * Retrieves the specified data from the storage backend. + * + * @param integer $field The field name of the desired data. + * See lib/Storage.php for the available fields. + * @param boolean $readonly Whether to disable any write operations. + * + * @return Ingo_Storage_rule|Ingo_Storage_filters The specified data. + */ + protected function _retrieve($field, $readonly = false) + { + switch ($field) { + case self::ACTION_BLACKLIST: + case self::ACTION_WHITELIST: + if ($field == self::ACTION_BLACKLIST) { + $ob = new Ingo_Storage_blacklist(); + $filters = &$this->retrieve(self::ACTION_FILTERS); + if (is_a($filters, 'PEAR_Error')) { + return $filters; + } + $rule = $filters->findRule($field); + if (isset($rule['action-value'])) { + $ob->setBlacklistFolder($rule['action-value']); + } + } else { + $ob = new Ingo_Storage_whitelist(); + } + $query = sprintf('SELECT list_address FROM %s WHERE list_owner = ? AND list_blacklist = ?', + $this->_params['table_lists']); + $values = array(Ingo::getUser(), + (int)($field == self::ACTION_BLACKLIST)); + Horde::logMessage('Ingo_Storage_sql::_retrieve(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $addresses = $this->_db->getCol($query, 0, $values); + if (is_a($addresses, 'PEAR_Error')) { + Horde::logMessage($addresses, __FILE__, __LINE__, PEAR_LOG_ERR); + return $addresses; + } + if ($field == self::ACTION_BLACKLIST) { + $ob->setBlacklist($addresses, false); + } else { + $ob->setWhitelist($addresses, false); + } + break; + + case self::ACTION_FILTERS: + $ob = new Ingo_Storage_filters_sql($this->_db, $this->_write_db, $this->_params); + if (is_a($result = $ob->init($readonly), 'PEAR_Error')) { + return $result; + } + break; + + case self::ACTION_FORWARD: + $query = sprintf('SELECT * FROM %s WHERE forward_owner = ?', + $this->_params['table_forwards']); + Horde::logMessage('Ingo_Storage_sql::_retrieve(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->query($query, Ingo::getUser()); + $data = $result->fetchRow(DB_FETCHMODE_ASSOC); + + $ob = new Ingo_Storage_forward(); + if ($data && !is_a($data, 'PEAR_Error')) { + $ob->setForwardAddresses(explode("\n", $data['forward_addresses']), false); + $ob->setForwardKeep((bool)$data['forward_keep']); + $ob->setSaved(true); + } + break; + + case self::ACTION_VACATION: + $query = sprintf('SELECT * FROM %s WHERE vacation_owner = ?', + $this->_params['table_vacations']); + Horde::logMessage('Ingo_Storage_sql::_retrieve(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->query($query, Ingo::getUser()); + $data = $result->fetchRow(DB_FETCHMODE_ASSOC); + + $ob = new Ingo_Storage_vacation(); + if ($data && !is_a($data, 'PEAR_Error')) { + $ob->setVacationAddresses(explode("\n", $data['vacation_addresses']), false); + $ob->setVacationDays((int)$data['vacation_days']); + $ob->setVacationStart((int)$data['vacation_start']); + $ob->setVacationEnd((int)$data['vacation_end']); + $ob->setVacationExcludes(explode("\n", $data['vacation_excludes']), false); + $ob->setVacationIgnorelist((bool)$data['vacation_ignorelists']); + $ob->setVacationReason(String::convertCharset($data['vacation_reason'], $this->_params['charset'])); + $ob->setVacationSubject(String::convertCharset($data['vacation_subject'], $this->_params['charset'])); + $ob->setSaved(true); + } + break; + + case self::ACTION_SPAM: + $query = sprintf('SELECT * FROM %s WHERE spam_owner = ?', + $this->_params['table_spam']); + Horde::logMessage('Ingo_Storage_sql::_retrieve(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->query($query, Ingo::getUser()); + $data = $result->fetchRow(DB_FETCHMODE_ASSOC); + + $ob = new Ingo_Storage_spam(); + if ($data && !is_a($data, 'PEAR_Error')) { + $ob->setSpamFolder($data['spam_folder']); + $ob->setSpamLevel((int)$data['spam_level']); + $ob->setSaved(true); + } + break; + + default: + $ob = false; + } + + return $ob; + } + + /** + * Stores the specified data in the storage backend. + * + * @access private + * + * @param Ingo_Storage_rule|Ingo_Storage_filters $ob The object to store. + * + * @return boolean True on success. + */ + protected function _store(&$ob) + { + switch ($ob->obType()) { + case self::ACTION_BLACKLIST: + case self::ACTION_WHITELIST: + $is_blacklist = (int)($ob->obType() == self::ACTION_BLACKLIST); + if ($is_blacklist) { + $filters = &$this->retrieve(self::ACTION_FILTERS); + if (is_a($filters, 'PEAR_Error')) { + return $filters; + } + $id = $filters->findRuleId(self::ACTION_BLACKLIST); + if ($id !== null) { + $rule = $filters->getRule($id); + if (!isset($rule['action-value']) || + $rule['action-value'] != $ob->getBlacklistFolder()) { + $rule['action-value'] = $ob->getBlacklistFolder(); + $filters->updateRule($rule, $id); + } + } + } + $query = sprintf('DELETE FROM %s WHERE list_owner = ? AND list_blacklist = ?', + $this->_params['table_lists']); + $values = array(Ingo::getUser(), $is_blacklist); + Horde::logMessage('Ingo_Storage_sql::_store(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + $query = sprintf('INSERT INTO %s (list_owner, list_blacklist, list_address) VALUES (?, ?, ?)', + $this->_params['table_lists']); + Horde::logMessage('Ingo_Storage_sql::_store(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $addresses = $is_blacklist ? $ob->getBlacklist() : $ob->getWhitelist(); + foreach ($addresses as $address) { + $result = $this->_write_db->query($query, + array(Ingo::getUser(), + $is_blacklist, + $address)); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + } + $ob->setSaved(true); + $ret = true; + break; + + case self::ACTION_FILTERS: + $ret = true; + break; + + case self::ACTION_FORWARD: + if ($ob->isSaved()) { + $query = 'UPDATE %s SET forward_addresses = ?, forward_keep = ? WHERE forward_owner = ?'; + } else { + $query = 'INSERT INTO %s (forward_addresses, forward_keep, forward_owner) VALUES (?, ?, ?)'; + } + $query = sprintf($query, $this->_params['table_forwards']); + $values = array( + implode("\n", $ob->getForwardAddresses()), + (int)(bool)$ob->getForwardKeep(), + Ingo::getUser()); + Horde::logMessage('Ingo_Storage_sql::_store(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $ret = $this->_write_db->query($query, $values); + if (!is_a($ret, 'PEAR_Error')) { + $ob->setSaved(true); + } + break; + + case self::ACTION_VACATION: + if ($ob->isSaved()) { + $query = 'UPDATE %s SET vacation_addresses = ?, vacation_subject = ?, vacation_reason = ?, vacation_days = ?, vacation_start = ?, vacation_end = ?, vacation_excludes = ?, vacation_ignorelists = ? WHERE vacation_owner = ?'; + } else { + $query = 'INSERT INTO %s (vacation_addresses, vacation_subject, vacation_reason, vacation_days, vacation_start, vacation_end, vacation_excludes, vacation_ignorelists, vacation_owner) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + } + $query = sprintf($query, $this->_params['table_vacations']); + $values = array( + implode("\n", $ob->getVacationAddresses()), + String::convertCharset($ob->getVacationSubject(), + NLS::getCharset(), + $this->_params['charset']), + String::convertCharset($ob->getVacationReason(), + NLS::getCharset(), + $this->_params['charset']), + (int)$ob->getVacationDays(), + (int)$ob->getVacationStart(), + (int)$ob->getVacationEnd(), + implode("\n", $ob->getVacationExcludes()), + (int)(bool)$ob->getVacationIgnorelist(), + Ingo::getUser()); + Horde::logMessage('Ingo_Storage_sql::_store(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $ret = $this->_write_db->query($query, $values); + if (!is_a($ret, 'PEAR_Error')) { + $ob->setSaved(true); + } + break; + + case self::ACTION_SPAM: + if ($ob->isSaved()) { + $query = 'UPDATE %s SET spam_level = ?, spam_folder = ? WHERE spam_owner = ?'; + } else { + $query = 'INSERT INTO %s (spam_level, spam_folder, spam_owner) VALUES (?, ?, ?)'; + } + $query = sprintf($query, $this->_params['table_spam']); + $values = array( + (int)$ob->getSpamLevel(), + $ob->getSpamFolder(), + Ingo::getUser()); + Horde::logMessage('Ingo_Storage_sql::_store(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $ret = $this->_write_db->query($query, $values); + if (!is_a($ret, 'PEAR_Error')) { + $ob->setSaved(true); + } + break; + + default: + $ret = false; + break; + } + + if (is_a($ret, 'PEAR_Error')) { + Horde::logMessage($ret, __FILE__, __LINE__); + } + + return $ret; + } + +} + +/** + * Ingo_Storage_filters_sql is the object used to hold user-defined filtering + * rule information. + * + * @author Jan Schneider + * @package Ingo + */ +class Ingo_Storage_filters_sql extends Ingo_Storage_filters { + + /** + * Handle for the current database connection. + * + * @var DB + */ + protected $_db; + + /** + * Handle for the current database connection, used for writing. + * + * Defaults to the same handle as $_db if a separate write database is not + * required. + * + * @var DB + */ + protected $_write_db; + + /** + * Driver specific parameters. + * + * @var array + */ + protected $_params; + + /** + * Constructor. + * + * @param DB $db Handle for the database connection. + * @param DB $write_db Handle for the database connection, used for + * writing. + * @param array $params Driver specific parameters. + */ + public function __construct($db, $write_db, $params) + { + $this->_db = $db; + $this->_write_db = $write_db; + $this->_params = $params; + } + + /** + * Loads all rules from the DB backend. + * + * @param boolean $readonly Whether to disable any write operations. + */ + public function init($readonly = false) + { + $query = sprintf('SELECT * FROM %s WHERE rule_owner = ? ORDER BY rule_order', + $this->_params['table_rules']); + $values = array(Ingo::getUser()); + Horde::logMessage('Ingo_Storage_filters_sql(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $data = array(); + while ($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) { + $data[$row['rule_order']] = array( + 'id' => (int)$row['rule_id'], + 'name' => String::convertCharset($row['rule_name'], $this->_params['charset']), + 'action' => (int)$row['rule_action'], + 'action-value' => String::convertCharset($row['rule_value'], $this->_params['charset']), + 'flags' => (int)$row['rule_flags'], + 'conditions' => empty($row['rule_conditions']) ? null : String::convertCharset(unserialize($row['rule_conditions']), $this->_params['charset']), + 'combine' => (int)$row['rule_combine'], + 'stop' => (bool)$row['rule_stop'], + 'disable' => !(bool)$row['rule_active']); + } + $this->setFilterlist($data); + + if (empty($data) && !$readonly) { + $data = @unserialize($GLOBALS['prefs']->getDefault('rules')); + if ($data) { + foreach ($data as $val) { + $this->addRule($val, false); + } + } else { + $this->addRule( + array('name' => 'Whitelist', + 'action' => Ingo_Storage::ACTION_WHITELIST), + false); + $this->addRule( + array('name' => 'Vacation', + 'action' => Ingo_Storage::ACTION_VACATION, + 'disable' => true), + false); + $this->addRule( + array('name' => 'Blacklist', + 'action' => Ingo_Storage::ACTION_BLACKLIST), + false); + $this->addRule( + array('name' => 'Spam Filter', + 'action' => Ingo_Storage::ACTION_SPAM, + 'disable' => true), + false); + $this->addRule( + array('name' => 'Forward', + 'action' => Ingo_Storage::ACTION_FORWARD), + false); + } + } + } + + /** + * Converts a rule hash from Ingo's internal format to the database + * format. + * + * @param array $rule Rule hash in Ingo's format. + * + * @return array Rule hash in DB's format. + */ + protected function _ruleToBackend($rule) + { + return array(String::convertCharset($rule['name'], NLS::getCharset(), $this->_params['charset']), + (int)$rule['action'], + isset($rule['action-value']) ? String::convertCharset($rule['action-value'], NLS::getCharset(), $this->_params['charset']) : null, + isset($rule['flags']) ? (int)$rule['flags'] : null, + isset($rule['conditions']) ? serialize(String::convertCharset($rule['conditions'], NLS::getCharset(), $this->_params['charset'])) : null, + isset($rule['combine']) ? (int)$rule['combine'] : null, + isset($rule['stop']) ? (int)$rule['stop'] : null, + isset($rule['disable']) ? (int)(!$rule['disable']) : 1); + } + + /** + * Adds a rule hash to the filters list. + * + * @param array $rule A rule hash. + * @param boolean $default If true merge the rule hash with default rule + * values. + */ + public function addRule($rule, $default = true) + { + if ($default) { + $rule = array_merge($this->getDefaultRule(), $rule); + } + + $query = sprintf('INSERT INTO %s (rule_id, rule_owner, rule_name, rule_action, rule_value, rule_flags, rule_conditions, rule_combine, rule_stop, rule_active, rule_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + $this->_params['table_rules']); + $id = $this->_write_db->nextId($this->_params['table_rules']); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + $order = key(array_reverse($this->_filters, true)) + 1; + $values = array_merge(array($id, Ingo::getUser()), + $this->_ruleToBackend($rule), + array($order)); + Horde::logMessage('Ingo_Storage_filters_sql::addRule(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $rule['id'] = $id; + $this->_filters[$order] = $rule; + } + + /** + * Updates an existing rule with a rule hash. + * + * @param array $rule A rule hash + * @param integer $id A rule number + */ + public function updateRule($rule, $id) + { + $query = sprintf('UPDATE %s SET rule_name = ?, rule_action = ?, rule_value = ?, rule_flags = ?, rule_conditions = ?, rule_combine = ?, rule_stop = ?, rule_active = ?, rule_order = ? WHERE rule_id = ? AND rule_owner = ?', + $this->_params['table_rules']); + $values = array_merge($this->_ruleToBackend($rule), + array($id, $rule['id'], Ingo::getUser())); + Horde::logMessage('Ingo_Storage_filters_sql::updateRule(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $this->_filters[$id] = $rule; + } + + /** + * Deletes a rule from the filters list. + * + * @param integer $id Number of the rule to delete. + * + * @return boolean True if the rule has been found and deleted. + */ + public function deleteRule($id) + { + if (!isset($this->_filters[$id])) { + return false; + } + + $query = sprintf('DELETE FROM %s WHERE rule_id = ? AND rule_owner = ?', + $this->_params['table_rules']); + $values = array($this->_filters[$id]['id'], Ingo::getUser()); + Horde::logMessage('Ingo_Storage_filters_sql::deleteRule(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + unset($this->_filters[$id]); + + $query = sprintf('UPDATE %s SET rule_order = rule_order - 1 WHERE rule_owner = ? AND rule_order > ?', + $this->_params['table_rules']); + $values = array(Ingo::getUser(), $id); + Horde::logMessage('Ingo_Storage_filters_sql::deleteRule(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + return true; + } + + /** + * Creates a copy of an existing rule. + * + * The created copy is added to the filters list right after the original + * rule. + * + * @param integer $id Number of the rule to copy. + * + * @return boolean True if the rule has been found and copied. + */ + public function copyRule($id) + { + if (isset($this->_filters[$id])) { + $newrule = $this->_filters[$id]; + $newrule['name'] = sprintf(_("Copy of %s"), $this->_filters[$id]['name']); + $this->addRule($newrule, false); + $this->ruleUp(count($this->_filters) - 1, count($this->_filters) - $id - 2); + return true; + } + + return false; + } + + /** + * Moves a rule up in the filters list. + * + * @param integer $id Number of the rule to move. + * @param integer $steps Number of positions to move the rule up. + */ + public function ruleUp($id, $steps = 1) + { + return $this->_ruleMove($id, -$steps); + } + + /** + * Moves a rule down in the filters list. + * + * @param integer $id Number of the rule to move. + * @param integer $steps Number of positions to move the rule down. + */ + public function ruleDown($id, $steps = 1) + { + return $this->_ruleMove($id, $steps); + } + + /** + * Moves a rule in the filters list. + * + * @param integer $id Number of the rule to move. + * @param integer $steps Number of positions and direction to move the + * rule. + */ + protected function _ruleMove($id, $steps) + { + $query = sprintf('UPDATE %s SET rule_order = rule_order %s 1 WHERE rule_owner = ? AND rule_order %s ? AND rule_order %s ?', + $this->_params['table_rules'], + $steps > 0 ? '-' : '+', + $steps > 0 ? '>' : '>=', + $steps > 0 ? '<=' : '<'); + $values = array(Ingo::getUser()); + if ($steps < 0) { + $values[] = (int)($id + $steps); + $values[] = (int)$id; + } else { + $values[] = (int)$id; + $values[] = (int)($id + $steps); + } + Horde::logMessage('Ingo_Storage_filters_sql::ruleUp(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + $query = sprintf('UPDATE %s SET rule_order = ? WHERE rule_owner = ? AND rule_id = ?', + $this->_params['table_rules']); + $values = array((int)($id + $steps), + Ingo::getUser(), + $this->_filters[$id]['id']); + Horde::logMessage('Ingo_Storage_filters_sql::ruleUp(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $this->init(); + } + + /** + * Disables a rule. + * + * @param integer $id Number of the rule to disable. + */ + public function ruleDisable($id) + { + $rule = $this->_filters[$id]; + $rule['disable'] = true; + $this->updateRule($rule, $id); + } + + /** + * Enables a rule. + * + * @param integer $id Number of the rule to enable. + */ + public function ruleEnable($id) + { + $rule = $this->_filters[$id]; + $rule['disable'] = false; + $this->updateRule($rule, $id); + } + +} diff --git a/ingo/lib/Template.php b/ingo/lib/Template.php new file mode 100644 index 000000000..71effba46 --- /dev/null +++ b/ingo/lib/Template.php @@ -0,0 +1,511 @@ +. + * + * Horde_Template provides a basic template engine with tags, loops, + * and if conditions. However, it is also a simple interface with + * several essential functions: set(), fetch(), and + * parse(). Subclasses or decorators can implement (or delegate) these + * three methods, plus the options api, and easily implement other + * template engines (PHP code, XSLT, etc.) without requiring usage + * changes. + * + * Compilation code adapted from code written by Bruno Pedro . + * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Chuck Hagenbuch + * @author Michael Slusarz + * @package Horde_Template + */ + +/** The identifier to use for memory-only templates. */ +define('INGO_TEMPLATE_STRING', '**string'); + +class Ingo_Template { + + /** + * The Horde_Cache object to use. + * + * @var Horde_Cache + */ + var $_cache; + + /** + * Option values. + * + * @var array + */ + var $_options = array(); + + /** + * Directory that templates should be read from. + * + * @var string + */ + var $_basepath = ''; + + /** + * Tag (scalar) values. + * + * @var array + */ + var $_scalars = array(); + + /** + * Loop tag values. + * + * @var array + */ + var $_arrays = array(); + + /** + * Path to template source. + * + * @var string + */ + var $_templateFile = null; + + /** + * Template source. + * + * @var string + */ + var $_template = null; + + /** + * Foreach variable mappings. + * + * @var array + */ + var $_foreachMap = array(); + + /** + * Foreach variable incrementor. + * + * @var integer + */ + var $_foreachVar = 0; + + /** + * preg_match() cache. + * + * @var array + */ + var $_pregcache = array(); + + /** + * Constructor. + * + * @param string $basepath The directory where templates are read from. + */ + function Ingo_Template($basepath = null) + { + if (!is_null($basepath)) { + $this->_basepath = $basepath; + } + + if (!empty($GLOBALS['conf']['cache']['driver'])) { + require_once 'Horde/Cache.php'; + $this->_cache = &Horde_Cache::singleton($GLOBALS['conf']['cache']['driver'], Horde::getDriverConfig('cache', $GLOBALS['conf']['cache']['driver'])); + } + + // DEBUGGING + $this->setOption('forcecompile', true); + } + + /** + * Sets an option. + * Currently available options are: + *
+     * 'debug' - Output debugging information to screen
+     * 'forcecompile' - Force a compilation on every page load
+     * 'gettext' - Activate gettext detection
+     * 
+     *
+     * @param string $option  The option name.
+     * @param mixed $val      The option's value.
+     */
+    function setOption($option, $val)
+    {
+        $this->_options[$option] = $val;
+    }
+
+    /**
+     * Set the template contents to a string.
+     *
+     * @param string $template  The template text.
+     */
+    function setTemplate($template)
+    {
+        $this->_template = $template;
+        $this->_parse();
+        $this->_templateFile = INGO_TEMPLATE_STRING;
+    }
+
+    /**
+     * Returns an option's value.
+     *
+     * @param string $option  The option name.
+     *
+     * @return mixed  The option's value.
+     */
+    function getOption($option)
+    {
+        return isset($this->_options[$option]) ? $this->_options[$option] : null;
+    }
+
+    /**
+     * Sets a tag, loop, or if variable.
+     *
+     * @param string|array $tag   Either the tag name or a hash with tag names
+     *                            as keys and tag values as values.
+     * @param mixed        $var   The value to replace the tag with.
+     */
+    function set($tag, $var)
+    {
+        if (is_array($tag)) {
+            foreach ($tag as $tTag => $tVar) {
+                $this->set($tTag, $tVar);
+            }
+        } elseif (is_array($var) || is_object($var)) {
+            $this->_arrays[$tag] = $var;
+        } else {
+            $this->_scalars[$tag] = $var;
+        }
+    }
+
+    /**
+     * Returns the value of a tag or loop.
+     *
+     * @param string $tag  The tag name.
+     *
+     * @return mixed  The tag value or null if the tag hasn't been set yet.
+     */
+    function get($tag)
+    {
+        if (isset($this->_arrays[$tag])) {
+            return $this->_arrays[$tag];
+        }
+        if (isset($this->_scalars[$tag])) {
+            return $this->_scalars[$tag];
+        }
+        return null;
+    }
+
+    /**
+     * Fetches a template from the specified file and return the parsed
+     * contents.
+     *
+     * @param string $filename  The file to fetch the template from.
+     *
+     * @return string  The parsed template.
+     */
+    function fetch($filename = null)
+    {
+        $file = $this->_basepath . $filename;
+        $force = $this->getOption('forcecompile');
+
+        if (!is_null($filename) && ($file != $this->_templateFile)) {
+            $this->_template = $this->_templateFile = null;
+        }
+
+        /* First, check for a cached compiled version. */
+        if (!$force && is_null($this->_template) && isset($this->_cache)) {
+            $cacheid = 'horde_template|' . filemtime($file) . '|' . $file;
+            $this->_template = $this->_cache->get($cacheid, 0);
+            if ($this->_template === false) {
+                $this->_template = null;
+            }
+        }
+
+        /* Parse and compile the template. */
+        if ($force || is_null($this->_template)) {
+            $this->_template = str_replace("\n", " \n", file_get_contents($file));
+            $this->_parse();
+            if (isset($cacheid) &&
+                !$this->_cache->set($cacheid, $this->_template)) {
+                Horde::logMessage(sprintf(_("Could not save the compiled template file '%s'."), $file), __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        $this->_templateFile = $file;
+
+        /* Template debugging. */
+        if ($this->getOption('debug')) {
+            echo '
' . htmlspecialchars($this->_template) . '
'; + } + + return $this->parse(); + } + + /** + * Parses all variables/tags in the template. + * + * @param string $contents The unparsed template. + * + * @return string The parsed template. + */ + function parse($contents = null) + { + if (!is_null($contents)) { + $this->setTemplate(str_replace("\n", " \n", $contents)); + } + + /* Evaluate the compiled template and return the output. */ + ob_start(); + eval('?>' . $this->_template); + return str_replace(" \n", "\n", ob_get_clean()); + } + + /** + * Parses all variables/tags in the template. + */ + function _parse() + { + // Escape XML instructions. + $this->_template = preg_replace('/\?>|<\?/', + '', + $this->_template); + + // Parse gettext tags, if the option is enabled. + if ($this->getOption('gettext')) { + $this->_parseGettext(); + } + + // Process ifs. + $this->_parseIf(); + + // Process loops and arrays. + $this->_parseLoop(); + + // Process base scalar tags. Needs to be after _parseLoop() as we + // rely on _foreachMap(). + $this->_parseTags(); + + // Finally, process any associative array scalar tags. + $this->_parseAssociativeTags(); + } + + /** + * Parses gettext tags. + * + * @access private + */ + function _parseGettext() + { + if (preg_match_all("/(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) { + $replace = array(); + foreach ($matches as $val) { + $replace[$val[0]] = ''; + } + $this->_doReplace($replace); + } + } + + /** + * Parses 'if' statements. + * + * @access private + * + * @param string $key The key prefix to parse. + */ + function _parseIf($key = null) + { + $replace = array(); + + foreach ($this->_doSearch('if', $key) as $val) { + $replace[$val[0]] = '_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>'; + $replace[$val[2]] = ''; + + // Check for else statement. + foreach ($this->_doSearch('else', $key) as $val2) { + $replace[$val2[0]] = ''; + $replace[$val2[2]] = ''; + } + } + + $this->_doReplace($replace); + } + + /** + * Parses the given array for any loops or other uses of the array. + * + * @access private + * + * @param string $key The key prefix to parse. + */ + function _parseLoop($key = null) + { + $replace = array(); + + foreach ($this->_doSearch('loop', $key) as $val) { + $divider = null; + + // See if we have a divider. + if (preg_match("/(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) { + $divider = $m[1]; + $replace[$m[0]] = ''; + } + + if (!isset($this->_foreachMap[$val[1]])) { + $this->_foreachMap[$val[1]] = ++$this->_foreachVar; + } + $varId = $this->_foreachMap[$val[1]]; + $var = $this->_generatePHPVar('arrays', $val[1]); + + $replace[$val[0]] = '_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>'; + $replace[$val[2]] = ''; + + // Parse ifs. + $this->_parseIf($val[1]); + + // Parse interior loops. + $this->_parseLoop($val[1]); + + // Replace scalars. + $this->_parseTags($val[1]); + } + + $this->_doReplace($replace); + } + + /** + * Replaces 'tag' tags with their PHP equivalents. + * + * @access private + * + * @param string $key The key prefix to parse. + */ + function _parseTags($key = null) + { + $replace = array(); + + foreach ($this->_doSearch('tag', $key, true) as $val) { + $replace_text = '_foreachMap[$val[1]])) { + $var = $this->_foreachMap[$val[1]]; + $replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else'; + } + $var = $this->_generatePHPVar('scalars', $val[1]); + $replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>'; + } + + $this->_doReplace($replace); + } + + /** + * Parse associative tags (i.e. ). + * + * @access private + */ + function _parseAssociativeTags() + { + $replace = array(); + + foreach ($this->_pregcache['tag'] as $key => $val) { + $parts = explode('.', $val[1]); + $var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']'; + $replace[$val[0]] = ''; + unset($this->_pregcache['tag'][$key]); + } + + $this->_doReplace($replace); + } + + /** + * Output the correct PHP variable string for use in template space. + * + * @access private + */ + function _generatePHPVar($tag, $key) + { + $out = ''; + + $a = explode('.', $key); + $a_count = count($a); + + if ($a_count == 1) { + switch ($tag) { + case 'arrays': + $out = '$this->_arrays'; + break; + + case 'scalars': + $out = '$this->_scalars'; + break; + } + } else { + $out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))]; + } + + return $out . '[\'' . end($a) . '\']'; + } + + /** + * TODO + * + * @access private + */ + function _doSearch($tag, $key, $noclose = false) + { + $out = array(); + $level = (is_null($key)) ? 0 : substr_count($key, '.') + 1; + + if (!isset($this->_pregcache[$key])) { + $regex = ($noclose) ? + "/<" . $tag . ":(.+?)\s\/>/" : + "/<" . $tag . ":([^>]+)>/"; + preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER); + } + + foreach ($this->_pregcache[$tag] as $pkey => $val) { + $val_level = substr_count($val[1], '.'); + $add = false; + if (is_null($key)) { + $add = !$val_level; + } else { + $add = (($val_level == $level) && + (strpos($val[1], $key . '.') === 0)); + } + if ($add) { + if (!$noclose) { + $val[2] = ''; + } + $out[] = $val; + unset($this->_pregcache[$tag][$pkey]); + } + } + + return $out; + } + + /** + * TODO + * + * @access private + */ + function _doReplace($replace) + { + if (empty($replace)) { + return; + } + + $search = array(); + + foreach (array_keys($replace) as $val) { + $search[] = '/' . preg_quote($val, '/') . '/'; + } + + $this->_template = preg_replace($search, array_values($replace), $this->_template); + } + +} diff --git a/ingo/lib/UI/VarRenderer/ingo.php b/ingo/lib/UI/VarRenderer/ingo.php new file mode 100644 index 000000000..f4d77ccb0 --- /dev/null +++ b/ingo/lib/UI/VarRenderer/ingo.php @@ -0,0 +1,20 @@ + + * @package Ingo + */ +class Horde_UI_VarRenderer_ingo extends Horde_UI_VarRenderer_html +{ + function _renderVarInput_ingo_folders(&$form, &$var, &$vars) + { + return Ingo::flistSelect($var->type->getFolder(), 'horde_form', 'folder'); + } +} diff --git a/ingo/lib/api.php b/ingo/lib/api.php new file mode 100644 index 000000000..7f0fe7128 --- /dev/null +++ b/ingo/lib/api.php @@ -0,0 +1,288 @@ + array(), + 'type' => '{urn:horde}stringArray'); + +$_services['blacklistFrom'] = array( + 'args' => array('addresses' => '{urn:horde}stringArray'), + 'type' => 'boolean', +); + +$_services['showBlacklist'] = array( + 'link' => '%application%/blacklist.php', +); + +$_services['whitelistFrom'] = array( + 'args' => array('addresses' => '{urn:horde}stringArray'), + 'type' => 'boolean', +); + +$_services['showWhitelist'] = array( + 'link' => '%application%/whitelist.php', +); + +$_services['canApplyFilters'] = array( + 'args' => array(), + 'type' => 'boolean', +); + +$_services['applyFilters'] = array( + 'args' => array('params' => '{urn:horde}stringArray'), + 'type' => 'boolean', +); + +$_services['showFilters'] = array( + 'link' => '%application%/filters.php', +); + +$_services['showVacation'] = array( + 'link' => '%application%/vacation.php', +); + +$_services['setVacation'] = array( + 'args' => array('info' => '{urn:horde}stringArray'), + 'type' => 'boolean', +); + +$_services['disableVacation'] = array( + 'args' => array(), + 'type' => 'boolean', +); + +/** + * Returns a list of available permissions. + * + * @return array An array describing all available permissions. + */ +function _ingo_perms() +{ + $perms = array(); + $perms['tree']['ingo']['allow_rules'] = false; + $perms['title']['ingo:allow_rules'] = _("Allow Rules"); + $perms['type']['ingo:allow_rules'] = 'boolean'; + $perms['tree']['ingo']['max_rules'] = false; + $perms['title']['ingo:max_rules'] = _("Maximum Number of Rules"); + $perms['type']['ingo:max_rules'] = 'int'; + + return $perms; +} + +/** + * Add addresses to the blacklist + * + * @param string $addresses The addresses to add + */ +function _ingo_blacklistFrom($addresses) +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + if (!empty($GLOBALS['ingo_shares'])) { + $_SESSION['ingo']['current_share'] = $signature; + } + + global $ingo_storage; + + /* Check for '@' entries in $addresses - this would call all mail to + * be blacklisted which is most likely not what is desired. */ + $addresses = array_unique($addresses); + $key = array_search('@', $addresses); + if ($key !== false) { + unset($addresses[$key]); + } + + if (!empty($addresses)) { + $blacklist = &$ingo_storage->retrieve(Ingo_Storage::ACTION_BLACKLIST); + $ret = $blacklist->setBlacklist(array_merge($blacklist->getBlacklist(), $addresses)); + if (is_a($ret, 'PEAR_Error')) { + $GLOBALS['notification']->push($ret, $ret->getCode()); + } else { + $ingo_storage->store($blacklist); + Ingo::updateScript(); + foreach ($addresses as $from) { + $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your blacklist."), $from)); + } + } + } +} + +/** + * Add addresses to the white list + * + * @param string $addresses The addresses to add + */ +function _ingo_whitelistFrom($addresses) +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + if (!empty($GLOBALS['ingo_shares'])) { + $_SESSION['ingo']['current_share'] = $signature; + } + + global $ingo_storage; + + $whitelist = &$ingo_storage->retrieve(Ingo_Storage::ACTION_WHITELIST); + $ret = $whitelist->setWhitelist(array_merge($whitelist->getWhitelist(), $addresses)); + if (is_a($ret, 'PEAR_Error')) { + $GLOBALS['notification']->push($ret, $ret->getCode()); + } else { + $ingo_storage->store($whitelist); + Ingo::updateScript(); + foreach ($addresses as $from) { + $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your whitelist."), $from)); + } + } +} + +/** + * Can this driver perform on-demand filtering? + * + * @return boolean True if perform() is available, false if not. + */ +function _ingo_canApplyFilters() +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + + $ingo_script = Ingo::loadIngoScript(); + if ($ingo_script) { + return $ingo_script->performAvailable(); + } else { + return false; + } +} + +/** + * Perform the filtering specified in the rules. + * + * @param array $params The parameter array. + * + * @return boolean True if filtering was performed, false if not. + */ +function _ingo_applyFilters($params = array()) +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + if (!empty($GLOBALS['ingo_shares'])) { + $_SESSION['ingo']['current_share'] = $signature; + } + + $ingo_script = Ingo::loadIngoScript(); + if ($ingo_script) { + return $ingo_script->perform($params); + } +} + +/** + * Set vacation + * + * @param array $info Vacation details + * + * @return boolean True on success. + */ +function _ingo_setVacation($info) +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + if (!empty($GLOBALS['ingo_shares'])) { + $_SESSION['ingo']['current_share'] = $signature; + } + + global $ingo_storage; + + /* Get vacation filter. */ + $filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); + $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION); + + if (empty($info)) { + return true; + } + + /* Set vacation object and rules. */ + $vacation = &$ingo_storage->retrieve(Ingo_Storage::ACTION_VACATION); + + /* Make sure we have at least one address. */ + if (empty($info['addresses'])) { + require_once 'Horde/Identity.php'; + $identity = &Identity::singleton('none'); + $info['addresses'] = implode("\n", $identity->getAll('from_addr')); + /* Remove empty lines. */ + $info['addresses'] = preg_replace('/\n+/', "\n", $info['addresses']); + if (empty($addresses)) { + $info['addresses'] = Auth::getAuth(); + } + } + + $vacation->setVacationAddresses($addresses); + + if (isset($info['days'])) { + $vacation->setVacationDays($info['days']); + } + if (isset($info['excludes'])) { + $vacation->setVacationExcludes($info['excludes']); + } + if (isset($info['ignorelist'])) { + $vacation->setVacationIgnorelist(($info['ignorelist'] == 'on')); + } + if (isset($info['reason'])) { + $vacation->setVacationReason($info['reason']); + } + if (isset($info['subject'])) { + $vacation->setVacationSubject($info['subject']); + } + if (isset($info['start'])) { + $vacation->setVacationStart($info['start']); + } + if (isset($info['end'])) { + $vacation->setVacationEnd($info['end']); + } + + $filters->ruleEnable($vacation_rule_id); + $result = $ingo_storage->store($filters); + if (!is_a($result, 'PEAR_Error')) { + if ($GLOBALS['prefs']->getValue('auto_update')) { + Ingo::updateScript(); + } + + /* Update the timestamp for the rules. */ + $_SESSION['ingo']['change'] = time(); + } + + return $result; +} + +/** + * Disable vacation + * + * @return boolean True on success. + */ +function _ingo_disableVacation() +{ + require_once dirname(__FILE__) . '/../lib/base.php'; + if (!empty($GLOBALS['ingo_shares'])) { + $_SESSION['ingo']['current_share'] = $signature; + } + + global $ingo_storage; + + /* Get vacation filter. */ + $filters = &$ingo_storage->retrieve(Ingo_Storage::ACTION_FILTERS); + $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION); + + $filters->ruleDisable($vacation_rule_id); + $result = $ingo_storage->store($filters); + if (!is_a($result, 'PEAR_Error')) { + if ($GLOBALS['prefs']->getValue('auto_update')) { + Ingo::updateScript(); + } + + /* Update the timestamp for the rules. */ + $_SESSION['ingo']['change'] = time(); + } + + return $result; +} diff --git a/ingo/lib/base.php b/ingo/lib/base.php new file mode 100644 index 000000000..7d1abd260 --- /dev/null +++ b/ingo/lib/base.php @@ -0,0 +1,104 @@ +pushApp('ingo', !defined('AUTH_HANDLER'))), 'PEAR_Error')) { + if ($pushed->getCode() == 'permission_denied') { + Horde::authenticationFailureRedirect(); + } + Horde::fatal($pushed, __FILE__, __LINE__, false); +} +$conf = &$GLOBALS['conf']; + +if (!defined('INGO_TEMPLATES')) { + define('INGO_TEMPLATES', $registry->get('templates')); +} + +// Notification system. +$notification = &Notification::singleton(); +$notification->attach('status'); + +// Redirect the user to the Horde login page if they haven't authenticated. +if (!Auth::isAuthenticated() && !defined('AUTH_HANDLER')) { + Horde::authenticationFailureRedirect(); +} + +// Start compression. +Horde::compressOutput(); + +// Load the Ingo_Storage driver. It appears in the global variable +// $ingo_storage. +$GLOBALS['ingo_storage'] = Ingo_Storage::factory(); + +// Create the ingo session (if needed). +if (!isset($_SESSION['ingo']) || !is_array($_SESSION['ingo'])) { + Ingo_Session::createSession(); +} + +// Create shares if necessary. +$driver = Ingo::getDriver(); +if ($driver->supportShares()) { + $GLOBALS['ingo_shares'] = &Horde_Share::singleton($registry->getApp()); + $GLOBALS['all_rulesets'] = Ingo::listRulesets(); + + /* If personal share doesn't exist then create it. */ + $signature = $_SESSION['ingo']['backend']['id'] . ':' . Auth::getAuth(); + if (!$GLOBALS['ingo_shares']->exists($signature)) { + require_once 'Horde/Identity.php'; + $identity = &Identity::singleton(); + $name = $identity->getValue('fullname'); + if (trim($name) == '') { + $name = Auth::removeHook(Auth::getAuth()); + } + $share = &$GLOBALS['ingo_shares']->newShare($signature); + $share->set('name', $name); + $GLOBALS['ingo_shares']->addShare($share); + $GLOBALS['all_rulesets'][$signature] = &$share; + } + + /* Select current share. */ + $_SESSION['ingo']['current_share'] = Util::getFormData('ruleset', @$_SESSION['ingo']['current_share']); + if (empty($_SESSION['ingo']['current_share']) || + empty($GLOBALS['all_rulesets'][$_SESSION['ingo']['current_share']]) || + !$GLOBALS['all_rulesets'][$_SESSION['ingo']['current_share']]->hasPermission(Auth::getAuth(), PERMS_READ)) { + $_SESSION['ingo']['current_share'] = $signature; + } +} else { + $GLOBALS['ingo_shares'] = null; +} diff --git a/ingo/lib/tests/MaildropTest.php b/ingo/lib/tests/MaildropTest.php new file mode 100644 index 000000000..3e4f5a0c2 --- /dev/null +++ b/ingo/lib/tests/MaildropTest.php @@ -0,0 +1,121 @@ + + * @package Ingo + * @subpackage UnitTests + */ + +require_once dirname(__FILE__) . '/TestBase.php'; + +class Ingo_MaildropTest extends Ingo_TestBase { + + function store($ob) + { + return $GLOBALS['ingo_storage']->store($ob); + } + + function setUp() + { + $GLOBALS['ingo_storage'] = &Ingo_Storage::factory('mock', + array('maxblacklist' => 3, + 'maxwhitelist' => 3)); + $GLOBALS['ingo_script'] = &Ingo_Script::factory('maildrop', array('path_style' => 'mbox')); + } + + function testForwardKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(true); + + $this->store($forward); + $this->assertScript('if( \ +/^From: .*/:h \ +) +exception { +cc "! joefabetes@example.com" +to "${DEFAULT}" +}'); + } + + function testForwardNoKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(false); + + $this->store($forward); + $this->assertScript('if( \ +/^From: .*/:h \ +) +exception { +cc "! joefabetes@example.com" +exit +}'); + } + + function testBlacklistWithFolder() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder('Junk'); + + $this->store($bl); + $this->assertScript('if( \ +/^From: .*spammer@example\.com/:h \ +) +exception { +to Junk +}'); + } + + function testBlacklistMarker() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(Ingo::BLACKLIST_MARKER); + + $this->store($bl); + $this->assertScript('if( \ +/^From: .*spammer@example\.com/:h \ +) +exception { +to ++DELETE++ +}'); + } + + function testBlacklistDiscard() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(null); + + $this->store($bl); + $this->assertScript('if( \ +/^From: .*spammer@example\.com/:h \ +) +exception { +to "/dev/null" +}'); + } + + function testWhitelist() + { + $wl = &new Ingo_Storage_whitelist(3); + $wl->setWhitelist(array('spammer@example.com')); + + $this->store($wl); + $this->assertScript('if( \ +/^From: .*spammer@example\.com/:h \ +) +exception { +to "${DEFAULT}" +}'); + } + +} diff --git a/ingo/lib/tests/ProcmailTest.php b/ingo/lib/tests/ProcmailTest.php new file mode 100644 index 000000000..2915b0f89 --- /dev/null +++ b/ingo/lib/tests/ProcmailTest.php @@ -0,0 +1,186 @@ + + * @package Ingo + * @subpackage UnitTests + */ + +require_once dirname(__FILE__) . '/TestBase.php'; + +class Ingo_ProcmailTest extends Ingo_TestBase { + + function store($ob) + { + return $GLOBALS['ingo_storage']->store($ob); + } + + function setUp() + { + $GLOBALS['conf']['spam'] = array('enabled' => true, + 'char' => '*', + 'header' => 'X-Spam-Level'); + $GLOBALS['ingo_storage'] = &Ingo_Storage::factory('mock', + array('maxblacklist' => 3, + 'maxwhitelist' => 3)); + $GLOBALS['ingo_script'] = &Ingo_Script::factory('procmail', + array('path_style' => 'mbox')); + } + + function testForwardKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(true); + + $this->store($forward); + $this->assertScript(':0 c +{ +:0 +*$ ! ^From *\/[^ ]+ +*$ ! ^Sender: *\/[^ ]+ +*$ ! ^From: *\/[^ ]+ +*$ ! ^Reply-to: *\/[^ ]+ +{ +OUTPUT = `formail -zxFrom:` +} +:0 E +{ +OUTPUT = $MATCH +} +:0 c +* !^FROM_MAILER +* !^X-Loop: to-joefabetes@example.com +| formail -A"X-Loop: to-joefabetes@example.com" | $SENDMAIL -oi -f $OUTPUT joefabetes@example.com +:0 E +$DEFAULT +:0 +/dev/null +}'); + } + + function testForwardNoKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(false); + + $this->store($forward); + $this->assertScript(':0 +{ +:0 +*$ ! ^From *\/[^ ]+ +*$ ! ^Sender: *\/[^ ]+ +*$ ! ^From: *\/[^ ]+ +*$ ! ^Reply-to: *\/[^ ]+ +{ +OUTPUT = `formail -zxFrom:` +} +:0 E +{ +OUTPUT = $MATCH +} +:0 c +* !^FROM_MAILER +* !^X-Loop: to-joefabetes@example.com +| formail -A"X-Loop: to-joefabetes@example.com" | $SENDMAIL -oi -f $OUTPUT joefabetes@example.com +:0 E +$DEFAULT +:0 +/dev/null +}'); + } + + function testBlacklistWithFolder() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder('Junk'); + + $this->store($bl); + $this->assertScript(':0 +* ^From:(.*\<)?spammer@example\.com +Junk'); + } + + function testBlacklistMarker() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(Ingo::BLACKLIST_MARKER); + + $this->store($bl); + $this->assertScript(':0 +* ^From:(.*\<)?spammer@example\.com +++DELETE++'); + } + + function testBlacklistDiscard() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(null); + + $this->store($bl); + $this->assertScript(':0 +* ^From:(.*\<)?spammer@example\.com +/dev/null'); + } + + function testWhitelist() + { + $wl = &new Ingo_Storage_whitelist(3); + $wl->setWhitelist(array('spammer@example.com')); + + $this->store($wl); + $this->assertScript(':0 +* ^From:(.*\<)?spammer@example\.com +$DEFAULT'); + } + + function testVacationDisabled() + { + $vacation = &new Ingo_Storage_vacation(); + $vacation->setVacationAddresses(array('from@example.com')); + $vacation->setVacationSubject('Subject'); + $vacation->setVacationReason("Because I don't like working!"); + + $this->store($vacation); + $this->assertScript(''); + } + + function testVacationEnabled() + { + $vacation = &new Ingo_Storage_vacation(); + $vacation->setVacationAddresses(array('from@example.com')); + $vacation->setVacationSubject('Subject'); + $vacation->setVacationReason("Because I don't like working!"); + + $this->store($vacation); + $this->_enableRule(INGO_STORAGE_ACTION_VACATION); + + $this->assertScript(':0 +{ +FILEDATE=`test -f \'.vacation.from@example.com\' && ls -lcn --time-style=+%s \'.vacation.from@example.com\' | awk \'{ print $6 + (604800) }\'` +DATE=`date +%s` +DUMMY=`test -f \'.vacation.from@example.com\' && test $FILEDATE -le $DATE && rm \'.vacation.from@example.com\'` +:0 Whc: vacation.lock +* $^To:(.*\<)?from@example.com +* !^X-Loop: from@example.com +* !^FROM_DAEMON +| formail -rD 8192 .vacation.from@example.com +:0 ehc +| (formail -rI"Precedence: junk" \ +-a"From: " \ +-A"X-Loop: from@example.com" \ +-i"Subject: Subject" ; \ +echo "Because I don\'t like working!" \ +) | $SENDMAIL -ffrom@example.com -oi -t +}'); + } + +} diff --git a/ingo/lib/tests/ScriptTest.php b/ingo/lib/tests/ScriptTest.php new file mode 100644 index 000000000..00f664e47 --- /dev/null +++ b/ingo/lib/tests/ScriptTest.php @@ -0,0 +1,339 @@ + + * @package Ingo + * @subpackage UnitTests + */ + +require_once dirname(__FILE__) . '/TestBase.php'; + +class Ingo_ScriptTest extends Ingo_TestBase { + + function test_blacklist_rule_without_folder_will_discard_matching_message() + { + $runner = ScriptTester::factory('all', $this); + + $ob = new Ingo_Storage_blacklist(); + $ob->setBlacklist(array('spammer@example.com')); + $ob->setBlacklistFolder(''); + $runner->addRule($ob); + + $runner->assertDeletesMessage('from_spammer'); + $runner->assertKeepsMessage('not_from_spammer'); + } + + function test_whitelist_rule_will_prevent_deletion_of_blacklisted_message() + { + $runner = ScriptTester::factory('all', $this); + + $bl = new Ingo_Storage_blacklist(); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(''); + $runner->addRule($bl); + + $wl = new Ingo_Storage_whitelist(); + $wl->setWhitelist(array('spammer@example.com')); + $runner->addRule($wl); + + $runner->assertKeepsMessage('from_spammer'); + $runner->assertKeepsMessage('not_from_spammer'); + } + + function test_blacklist_rule_with_folder_will_move_matching_messages() + { + $runner = ScriptTester::factory('all', $this); + + $ob = new Ingo_Storage_blacklist(); + $ob->setBlacklist(array('spammer@example.com')); + $ob->setBlacklistFolder('Junk'); + $runner->addRule($ob); + + $runner->assertMovesMessage('from_spammer', 'Junk'); + } + + function test_partial_whitelist_address_should_not_match() + { + $runner = ScriptTester::factory('all', $this); + + $bl = new Ingo_Storage_blacklist(); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(''); + $runner->addRule($bl); + + $wl = new Ingo_Storage_whitelist(); + $wl->setWhitelist(array('ammer@example.com')); + $runner->addRule($wl); + + $runner->assertDeletesMessage('from_spammer'); + } + + function test_partial_blacklist_address_should_not_match() + { + $runner = ScriptTester::factory('all', $this); + + $bl = new Ingo_Storage_blacklist(); + $bl->setBlacklist(array('ammer@example.com')); + $bl->setBlacklistFolder(''); + $runner->addRule($bl); + + $runner->assertKeepsMessage('from_spammer'); + } + +} + +/** + * Abstract base class for strategies for testing different Script backends + */ +class ScriptTester { + + var $test; + var $rules = array(); + + function ScriptTester($test) + { + $this->test = $test; + } + + function addRule($rule) + { + $this->rules[] = $rule; + } + + function assertDeletesMessage($fixture) + { + return PEAR::raiseError('Not implemented.'); + } + + function assertKeepsMessage($fixture) + { + return PEAR::raiseError('Not implemented.'); + } + + function assertMovesMessage($fixture, $to_folder) + { + return PEAR::raiseError('Not implemented.'); + } + + function factory($type, $test) + { + $class = 'ScriptTester_' . $type; + $ob = new $class($test); + return $ob; + } + + function _setupStorage() + { + $_SESSION['ingo']['change'] = 0; + $GLOBALS['ingo_storage'] = Ingo_Storage::factory('mock', array()); + foreach ($this->rules as $ob) { + $GLOBALS['ingo_storage']->store($ob); + } + } + +} + +/** + * Implementation of ScriptTester:: for testing 'imap' scripts + */ +class ScriptTester_imap extends ScriptTester { + + var $imap; + var $api; + + function _setup() + { + $this->_setupStorage(); + $this->api = Ingo_Script_imap_api::factory('mock', array()); + + $result = $this->api->loadFixtures(dirname(__FILE__) . '/_data/'); + $this->test->assertNotA($result, 'PEAR_Error'); + + $params = array('api' => $this->api); + $this->imap = Ingo_Script::factory('imap', $params); + } + + function _run() + { + $params = array('api' => $this->api); + $this->imap->perform($params); + } + + function assertDeletesMessage($fixture) + { + $this->_setup(); + $this->test->assertTrue($this->api->hasMessage($fixture)); + $this->_run(); + $this->test->assertFalse($this->api->hasMessage($fixture)); + } + + function assertKeepsMessage($fixture) + { + $this->_setup(); + $this->test->assertTrue($this->api->hasMessage($fixture)); + $this->_run(); + $this->test->assertTrue($this->api->hasMessage($fixture)); + } + + function assertMovesMessage($fixture, $to_folder) + { + $this->_setup(); + $this->test->assertTrue($this->api->hasMessage($fixture)); + $this->_run(); + $this->test->assertFalse($this->api->hasMessage($fixture)); + $this->test->assertTrue($this->api->hasMessage($fixture, $to_folder)); + } + +} + +/** + * This script tester iterates through all enabled backends to verify that + * each one works properly. + */ +class ScriptTester_all extends ScriptTester { + + var $backends = array('imap', 'sieve'); + + function _delegate($method, $params) + { + foreach ($this->backends as $backend) { + $runner = ScriptTester::factory($backend, $this->test); + foreach ($this->rules as $rule) { + $runner->addRule($rule); + } + call_user_func_array(array($runner, $method), $params); + } + } + + function assertDeletesMessage($fixture) + { + $this->_delegate('assertDeletesMessage', array($fixture)); + } + + function assertKeepsMessage($fixture) + { + $this->_delegate('assertKeepsMessage', array($fixture)); + } + + function assertMovesMessage($fixture, $to_folder) + { + $this->_delegate('assertMovesMessage', array($fixture, $to_folder)); + } + +} + +/** + * Test the sieve Script backend. This uses the command-line `sieve' from + * the GNU mailutils package. + */ +class ScriptTester_sieve extends ScriptTester { + + function assertDeletesMessage($fixture) + { + $this->_run(); + $this->_assertOutput("DISCARD on msg uid " . $this->uids[$fixture]); + } + + function assertKeepsMessage($fixture) + { + $this->_run(); + $this->_assertOutput("KEEP on msg uid " . $this->uids[$fixture]); + } + + function assertMovesMessage($fixture, $to_folder) + { + $this->_run(); + $this->_assertOutput("FILEINTO on msg uid " . $this->uids[$fixture] . + ": delivering into " . $to_folder); + } + + function _assertOutput($want) + { + $answer = $this->test->assertWantedPattern('/' . + preg_quote($want, '/') . '/', + $this->output); + if (!$answer) { + echo "FAILED SIEVE SCRIPT:\n\n", $this->sieve_text, "\n\n"; + } + } + + var $mbox; + var $sieve; + var $script_text; + var $output; + var $uids; + + function _run() + { + $this->_buildMailboxFile(); + $this->_writeSieveScript(); + $this->_runSieve(); + + @unlink($this->mbox); + @unlink($this->sieve); + } + + function _buildMailboxFile() + { + $this->uids = array(); + $this->mbox = tempnam('/tmp', 'mbox'); + $mh = fopen($this->mbox, 'w'); + $uid = 1; + + $dh = opendir(dirname(__FILE__) . '/_data'); + while (($dent = readdir($dh)) !== false) { + if ($dent == '.' || $dent == '..' || $dent == 'CVS') { + continue; + } + $filespec = dirname(__FILE__) . '/_data/' . $dent; + $fh = fopen($filespec, 'r'); + $data = fread($fh, filesize($filespec)); + fclose($fh); + + fwrite($mh, $data); + if ($data{strlen($data)-1} != "\n") { + fwrite($mh, "\n"); + } + + $this->uids[$dent] = $uid++; + } + closedir($dh); + + fclose($mh); + } + + function _writeSieveScript() + { + $params = array(); + + $this->_setupStorage(); + $script = Ingo_Script::factory('sieve', $params); + + $this->sieve = tempnam('/tmp', 'sieve'); + $fh = fopen($this->sieve, 'w'); + + $this->sieve_text = $script->generate(); + fwrite($fh, $this->sieve_text); + fclose($fh); + } + + function _runSieve() + { + $this->output = ''; + $ph = popen("sieve -vv -n -f " . escapeshellarg($this->mbox) . " " . + escapeshellarg($this->sieve), 'r'); + while (!feof($ph)) { + $data = fread($ph, 512); + if (is_string($data)) { + $this->output .= $data; + } + } + pclose($ph); + } + +} + diff --git a/ingo/lib/tests/SieveTest.php b/ingo/lib/tests/SieveTest.php new file mode 100644 index 000000000..3e2701535 --- /dev/null +++ b/ingo/lib/tests/SieveTest.php @@ -0,0 +1,137 @@ + + * @package Ingo + * @subpackage UnitTests + */ + +require_once dirname(__FILE__) . '/TestBase.php'; + +class Ingo_SieveTest extends Ingo_TestBase { + + function store($ob) + { + return $GLOBALS['ingo_storage']->store($ob); + } + + function setUp() + { + $GLOBALS['conf']['spam'] = array('enabled' => true, + 'char' => '*', + 'header' => 'X-Spam-Level'); + $GLOBALS['ingo_storage'] = &Ingo_Storage::factory('mock', + array('maxblacklist' => 3, + 'maxwhitelist' => 3)); + $GLOBALS['ingo_script'] = &Ingo_Script::factory('sieve', array()); + } + + function testForwardKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(true); + + $this->store($forward); + $this->assertScript('if true { +redirect "joefabetes@example.com"; +keep; +}'); + } + + function testForwardNoKeep() + { + $forward = &new Ingo_Storage_forward(); + $forward->setForwardAddresses('joefabetes@example.com'); + $forward->setForwardKeep(false); + + $this->store($forward); + $this->assertScript('if true { +redirect "joefabetes@example.com"; +}'); + } + + function testBlacklistMarker() + { + $bl = &new Ingo_Storage_blacklist(3); + $bl->setBlacklist(array('spammer@example.com')); + $bl->setBlacklistFolder(Ingo::BLACKLIST_MARKER); + + $this->store($bl); + $this->assertScript('require "imapflags"; +if address :all :comparator "i;ascii-casemap" :is ["From", "Sender", "Resent-From"] "spammer@example.com" { +addflag "\\\\Deleted"; +keep; +removeflag "\\\\Deleted"; +stop; +}'); + } + + function testWhitelist() + { + $wl = &new Ingo_Storage_whitelist(3); + $wl->setWhitelist(array('spammer@example.com')); + + $this->store($wl); + $this->assertScript('if address :all :comparator "i;ascii-casemap" :is ["From", "Sender", "Resent-From"] "spammer@example.com" { +keep; +stop; +}'); + } + + function testVacationDisabled() + { + $vacation = &new Ingo_Storage_vacation(); + $vacation->setVacationAddresses(array('from@example.com')); + $vacation->setVacationSubject('Subject'); + $vacation->setVacationReason("Because I don't like working!"); + + $this->store($vacation); + $this->assertScript(''); + } + + function testVacationEnabled() + { + $vacation = &new Ingo_Storage_vacation(); + $vacation->setVacationAddresses(array('from@example.com')); + $vacation->setVacationSubject('Subject'); + $vacation->setVacationReason("Because I don't like working!"); + + $this->store($vacation); + $this->_enableRule(INGO_STORAGE_ACTION_VACATION); + + $this->assertScript('require "vacation"; +if allof ( not exists ["list-help", "list-unsubscribe", "list-subscribe", "list-owner", "list-post", "list-archive", "list-id"], not header :comparator "i;ascii-casemap" :is "Precedence" "list,bulk" ) { +vacation :days 7 :addresses "from@example.com" :subject "Subject" "Because I don\'t like working!"; +}'); + } + + function testSpamDisabled() + { + $spam = &new Ingo_Storage_spam(); + $spam->setSpamLevel(7); + $spam->setSpamFolder("Junk"); + + $this->store($spam); + $this->assertScript(''); + } + + function testSpamEnabled() + { + $spam = &new Ingo_Storage_spam(); + $spam->setSpamLevel(7); + $spam->setSpamFolder("Junk"); + + $this->store($spam); + $this->_enableRule(INGO_STORAGE_ACTION_SPAM); + $this->assertScript('require "fileinto"; +if header :comparator "i;ascii-casemap" :contains "X-Spam-Level" "*******" { +fileinto "Junk"; +}'); + } + +} diff --git a/ingo/lib/tests/TestBase.php b/ingo/lib/tests/TestBase.php new file mode 100644 index 000000000..6cdad05b0 --- /dev/null +++ b/ingo/lib/tests/TestBase.php @@ -0,0 +1,51 @@ + + * @package Ingo + * @subpackage UnitTests + */ +class Ingo_TestBase extends PHPUnit_Framework_TestCase { + + function _enableRule($rule) + { + $filters = $GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS); + foreach ($filters->getFilterList() as $k => $v) { + if ($v['action'] == $rule) { + $v['disable'] = false; + $filters->updateRule($v, $k); + $this->store($filters); + } + } + } + + function assertScript($expect) + { + $result = $GLOBALS['ingo_script']->generate(); + if (!is_string($result)) { + $this->fail("result not a script", 1); + return; + } + + /* Remove comments and crunch whitespace so we can have a functional + * comparison. */ + $new = array(); + foreach (explode("\n", $result) as $line) { + if (preg_match('/^\s*$/', $line)) { + continue; + } + if (preg_match('/^\s*#.*$/', $line)) { + continue; + } + $new[] = trim($line); + } + + $new_script = join("\n", $new); + $this->assertEqual($expect, $new_script); + } + +} diff --git a/ingo/lib/tests/_data/from_spammer b/ingo/lib/tests/_data/from_spammer new file mode 100644 index 000000000..34572a94c --- /dev/null +++ b/ingo/lib/tests/_data/from_spammer @@ -0,0 +1,40 @@ +From cyrus@prometheus Mon Aug 7 15:01:35 2006 +Return-Path: +Received: from murder ([unix socket]) + by prometheus (Cyrus v2.2.4-Debian-2.2.4-2) with LMTPA; + Mon, 07 Aug 2006 15:01:35 -0400 +X-Sieve: CMU Sieve 2.2 +Received: from localhost (localhost.localdomain [127.0.0.1]) + by prometheus.cronosys.com (Postfix) with ESMTP id 61BCA7080C6 + for ; Mon, 7 Aug 2006 15:01:35 -0400 (EDT) +Received: from prometheus.cronosys.com ([127.0.0.1]) + by localhost (prometheus [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id 20060-03 for ; + Mon, 7 Aug 2006 15:01:35 -0400 (EDT) +Received: from localhost.localdomain (cousteau.att.cronosys.com [66.73.22.6]) + by prometheus.cronosys.com (Postfix) with ESMTP id 010CC7080C0 + for ; Mon, 7 Aug 2006 15:01:34 -0400 (EDT) +Received: by localhost.localdomain (Postfix, from userid 1000) + id 00D37C23; Mon, 7 Aug 2006 15:01:32 -0400 (EDT) +Date: Mon, 7 Aug 2006 15:01:32 -0400 +From: Bad Guy Spammer +To: jason.m.felice@gmail.com +Subject: This is a test +Message-ID: <20060807190132.GC22633@untamo.cronosys.com> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline +User-Agent: Mutt/1.5.6+20040907i +X-Virus-Scanned: by amavisd-new-20030616-p10 (Debian) at prometheus.cronosys.com +X-Spam-Status: No, hits=-1.4 tagged_above=-50.0 required=4.0 tests=AWL, + BAYES_00, FORGED_RCVD_HELO +X-Spam-Level: + +Buy dumb stuff now. + +-- + Jason Felice + (216) 221-4600 x302 + Cronosys -- Liberate the soul of your business . + + diff --git a/ingo/lib/tests/_data/not_from_spammer b/ingo/lib/tests/_data/not_from_spammer new file mode 100644 index 000000000..070120196 --- /dev/null +++ b/ingo/lib/tests/_data/not_from_spammer @@ -0,0 +1,40 @@ +From cyrus@prometheus Mon Aug 7 15:01:35 2006 +Return-Path: +Received: from murder ([unix socket]) + by prometheus (Cyrus v2.2.4-Debian-2.2.4-2) with LMTPA; + Mon, 07 Aug 2006 15:01:35 -0400 +X-Sieve: CMU Sieve 2.2 +Received: from localhost (localhost.localdomain [127.0.0.1]) + by prometheus.cronosys.com (Postfix) with ESMTP id 61BCA7080C6 + for ; Mon, 7 Aug 2006 15:01:35 -0400 (EDT) +Received: from prometheus.cronosys.com ([127.0.0.1]) + by localhost (prometheus [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id 20060-03 for ; + Mon, 7 Aug 2006 15:01:35 -0400 (EDT) +Received: from localhost.localdomain (cousteau.att.cronosys.com [66.73.22.6]) + by prometheus.cronosys.com (Postfix) with ESMTP id 010CC7080C0 + for ; Mon, 7 Aug 2006 15:01:34 -0400 (EDT) +Received: by localhost.localdomain (Postfix, from userid 1000) + id 00D37C23; Mon, 7 Aug 2006 15:01:32 -0400 (EDT) +Date: Mon, 7 Aug 2006 15:01:32 -0400 +From: Good Guy +To: jason.m.felice@gmail.com +Subject: This is a test +Message-ID: <20060807190132.GC22633@untamo.cronosys.com> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline +User-Agent: Mutt/1.5.6+20040907i +X-Virus-Scanned: by amavisd-new-20030616-p10 (Debian) at prometheus.cronosys.com +X-Spam-Status: No, hits=-1.4 tagged_above=-50.0 required=4.0 tests=AWL, + BAYES_00, FORGED_RCVD_HELO +X-Spam-Level: + +Buy not-so-dumb stuff now. + +-- + Jason Felice + (216) 221-4600 x302 + Cronosys -- Liberate the soul of your business . + + diff --git a/ingo/lib/version.php b/ingo/lib/version.php new file mode 100644 index 000000000..d5aedd3f2 --- /dev/null +++ b/ingo/lib/version.php @@ -0,0 +1 @@ + diff --git a/ingo/locale/ca_ES/LC_MESSAGES/ingo.mo b/ingo/locale/ca_ES/LC_MESSAGES/ingo.mo new file mode 100644 index 0000000000000000000000000000000000000000..69387cb40b659d1a4dfda5974f6411b72e8c85cb GIT binary patch literal 149951 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 literal 0 HcmV?d00001 diff --git a/ingo/locale/ca_ES/help.xml b/ingo/locale/ca_ES/help.xml new file mode 100755 index 000000000..309e8028a --- /dev/null +++ b/ingo/locale/ca_ES/help.xml @@ -0,0 +1,286 @@ + + + + + Llista negra + La llista negra és un llistat d'adreces de les quals no desitja veure missatges en la safata d'Entrada. + + + Llista negra: Acció + Aquesta acció defineix què fer al rebre un missatge d'un remitent bloquejat. Els valors possibles són "eliminar" o "traslladar a carpeta". Si es selecciona "eliminar", els missatges es descartaran abans d'emmagatzemar-los en la safata d'Entrada. Si es selecciona "Moure a carpeta", s'emmagatzemaran en la carpeta seleccionada. + + + Llista negra: Adreces + La llista d'adreces bloquejades. Cal introduir cada adreça en una línia diferent. + + + Filtres: Regles + Aquesta és la pantalla principal de regles. Aquí es poden crear regles prement el botó "Nova regla", reorganitzar l'ordre d'aplicació de les regles prement les fletxes cap amunt i cap avall o introduint noves posicions de les regles en la columna "Traslladar"; activar o desactivar regles específiques prement la icona de la columna "Activat" i modificar regles individuals prement la icona de la columna "Modificar" o el nom de la regla. + + Tingui en compte que les regles s'executen en l'ordre que es mostra aquí. Per exemple, si s'esborra un missatge a causa de la llista negra, les regles que segueixin ja no actuaran en aquest missatge + + + + Reenviar + + Pot optar per reexpedir de forma automàtica el seu correu entrant a una sèrie d'adreces de correu electrònic. + + + + Reenviar: Adreces + Reenviar: Adreces + + Pot optar per reexpedir el correu entrant a altre compte. Pot introduir tantes adreces com desitgi, cadascuna en una línia diferent. + + + + Reenviar: Mantenir una còpia + Reenviar: Mantenir una còpia + + Si activa aquesta opció, es conservarà una còpia dels missatges entrants en aquest compte al mateix temps que s'envien a les adreces a les quals desitja reenviar-les. + + + + Preferències: Mostrar avisos detallats al aplicar cada filtre? + + Si s'utilitza el driver IMAP de filtrat, aquesta preferència controla com d'informatiu serà el procés de filtrat. Si s'activa, es notificarà per pantalla cada missatge filtrat quan s'hagi processat. Si es desactiva, només es veurà a la pantalla un resum de les accions de filtrat. + + + + + Preferències: Filtrar només els missatges [no] llegits? + + Aquesta preferència identifica quan el controlador de filtrat IMAP intentarà aplicar regles en la safata d'Entrada. Pot aplicar els filtres a tots els missatges, NOMÉS a aquells marcats com no llegits o NOMÉS a aquells que s'han marcat com llegits . + + + + Regla de filtratge + + Una regla és el bloc bàsic d'elaboració dels seus filtres de correu, consisteix en una o diverses condicions i una o diverses accions. Quan se li envia un missatge, es processa a través de les condicions de les regles de filtrat i si compleix alguna, s'executen les accions indicades en aquest missatge. Els filtres poden ser molt fàcils per a eliminar de forma automàtica correu no desitjat o per a facilitar la gestió del seu compte de correu emmagatzemant els missatges relacionats en carpetes diferents. + + + + Regla de filtratge: Acció + + Aquestes són les accions que es poden portar a terme si un missatge entrant compleix la condició indicada. Tingui en compte que pot ser que no totes aquestes opcions estan disponibles - només es mostraran les opcions que funcionin amb el programari de filtrat utilitzat. + + Entregar a la meva bústia d'entrada + +El missatge es guardarà a la bústia a la seva safata d'entrada. És la opció per omisió. + + Entregar a la bústia + +El missatge s'emmagatzemarà en la carpeta indicada. + + Eliminar completament el missatge + + S'eliminarà silenciosament el missatge. Ni vostè ni el remitent rebran notificació alguna que ha succeït. + + Redirigir a + + El missatge es reenviarà a l'adreça especificada. No es conservarà cap còpia del missatge al seu compte. + + Entregar a la meva bústia d'entrega i redireccionar a + + El missatge s'enviarà a l'adreça especificada i es conservarà una còpia a la safata d'entrada. + + Rebutjar el motiu + + El missatge original s'eliminarà i s'enviarà un missatge nou al remitent amb el text que indiqui. + + + + Regla de filtrat: Combinar opcions + + Pot definir diverses condicions en una mateixa regla. Pot agrupar-les de forma lógica mitjançant "I"/"O", no pot crear filtres complexos que continguin ambdós tipus de condicions. + + I + + Si selecciona I, un missatge entrant ha de complir totes i cadascuna de les condicions que especifiqui perquè s'executin les accions. + + O + + Si selecciona O, les accions indicades s'executaran si es compleix almenys una de les condicions. + + + + Regla de filtratge: assenyalar missatge + + Com una de les accions d'una regla, pot indicar que es marqui un missatge amb un o més dels marcadors IMAP. Els valors possibles són: Llegit, Important, Respost i Eliminat. + + + + Regla de filtrat: Coincidència + + A cada condició d'una regla hi ha tres components. El primer en el camp que cal examinar. El segon és el tipus de comparació a realitzar. El tercer és el valor amb el qual cal comparar el camp. Existeixen diversos tipus diferents de comparances realizables. Les comparances disponibles per a cada camp depenen tant del tipus de camp com del que pugui manejar el programari utilitzat. Per això no totes les següents possibles opcions de coincidència apareixeran en un camp determinat. + + Conté + + Es considerarà cert si la cadena indicada es troba enlloc de la línia. Exemple: josep@exemple.com no conté usuari@exemple + + No conté + + És considerarà cert si la cadena indicada no es troba enlloc de la lína. Exemple: josep@exemple.com no conté usuari@exemple + + És + + Es considerarà cert si la cadena indicada coincideix exactament amb la línia. Exemple: usuari@exemple és usuari@exemple + + No és + + Es considerarà cert si la cadena indicada no coincideix exactament amb la línia. Exemple: usuari@exemple no és usuari@exemple.com + + Comença per + + Es considerarà cert si la cadena indicada coincideix amb el començament de la línia. Exemple: usuari@exemple.com Comença per usuari@exemple + + No comença per + + Es considerarà cert si la cadena indicada no coincideix amb el començament de la línia. Exemple: josep@exemple.com No comença per usuari@exemple + + Finalitza per + + Es considerarà cert si la cadena indicada coincideix amb el final de la línia. Exemple: usuari@exemple Acaba per @exemple + + No finalitza per + + Es considerarà cert si la cadena indicada no coincideix amb el final de la línia. Exemple: usuari@exemple No acaba per @exemple.com + + Existeix + +Es considerarà cert si la cadena indicada existeix en el missatge, independentment del que contingui. + + No existe + + És considerarà cert si la cadena indicada no existeix al missatge. + + Regex + + Li permet utilitzar en les comparacions de les capçaleres expressions regulars compatibles POSIX complexes. + Exemple: "Rebut des de [*\.*\.*\.*] per (amfitrio-a|amfitrio-b).exemple.com*" s'ha de cumplir amb "Rebut des de [172.16.100.1] per amfitrio-a.exemple.com el Dimarts" + + Coincideix + Coincideix és semblat a Conté amb la excepció que poden utilitzar-se * i ? com comodins. Un * substituirà qualsevol número de caràcters i una ? substituirà exactament un. Exemple: "*usuari?@exemple.com" es complirá tant amb "usuari1@exemple.com" com amb "otrousuario2@exemple.com" + + No coincideix + +No coincideix és el mateix que Coincideix excepte per que serà fals si el valor indicat coincideix amb la cadena de la capçalera del missatge. + + Menor que + +Aquesta és una prova relacional que compara numéricament el valor indicat i el valor de la capçalera del missatge. + + Menor o igual que + +Aquesta és una prova relacional que compara numéricament el valor indicat i el valor de la capçalera del missatge. + + Igual que + +Aquesta és una prova relacional que compara numéricament el valor indicat i el valor de la capçalera del missatge. + + Major o igual que + + Aquesta és una prova relacional que compara numéricament el valor indicat i el valor de la capçalera del missatge. + + Major que + + Aquesta és una prova relacional que compara numéricament el valor indicat i el valor de la capçalera del missatge. + + + + Regla de filtrat: Nom + +Es tracta d'un nom descriptiu d'una regla. Pot usar-lo per a identificar una regla determinada en el seu llistat de filtres. + + + + Regla de filtrat: Detenir comprovació + +Si s'activa aquesta opció i un missatge compleix la regla, la resta dels missatges no es processarán. + + + + Absència + +Els missatges d'absència són respostes automàticas enviades a la gent que li envíi correu. Normalment s'utilitzen quan va a estar absent per un llarg període de temps. + + + + Vacation Period + Vacation messages will only be sent during the period of time that you are on vacation. + + + Absència: No respondre a orri + +Activant aquesta opció les respostes per absència no s'enviaran amb missatges que semblin procedir de llistes de correu o que estiguin marcats com correu a orri. + + + + Absència: Interval de resposta + +És el nombre de dies a esperar abans de tornar a enviar una resposta automàtica a una adreça que ja hagi rebut una. + + + + Absència: Les meves adreces de correu + +Si té més d'una adreça de correu assignades a aquesta bústia, indiqueu-les aquí. + + + + Absència: Sense respostes + +No enviar la notificació d'absència a aquests destinataris. Cada adreça ha d'estar en una línia diferent. + + + + Absència: Motiu de l'absència + +És el text que s'enviarà en les seves notificacions d'absència. + + + + Absència: Assumpte de l'absència + +És l'assumpte que s'utilitzarà en les notificacions d'absència. + + + + Llista blanca + Llista blanca + +La llista blanca és un llistat d'adreces (legítimes) de les quals sempre desitja poder veure els missatges en la safata d'Entrada. Cada adreça ha d'incloure's en una línia distinta. + + + + Llista blanca: Adreces + + PENDENT. Cada adreça ha d'incloure's en una línia distinta. + + + + Spam Filtering: Spam Level + The system will consider messages with a likely spam score greater + than or equal to the number entered here as spam. + Lower numbers will catch more messages, with the drawback that there + is a greater chance of catching real messages. "5" is a typical value + if your system is using SpamAssassin. + + + Spam Filtering: Folder to receive spam + The system will file messages which it determines to be spam into + this folder. + + diff --git a/ingo/locale/cs_CZ/LC_MESSAGES/ingo.mo b/ingo/locale/cs_CZ/LC_MESSAGES/ingo.mo new file mode 100644 index 0000000000000000000000000000000000000000..af7c7d40fbe8f6f5e41988322a594ab6304cc2e4 GIT binary patch literal 146857 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 z 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 literal 0 HcmV?d00001 diff --git a/ingo/locale/da_DK/LC_MESSAGES/ingo.mo b/ingo/locale/da_DK/LC_MESSAGES/ingo.mo new file mode 100644 index 0000000000000000000000000000000000000000..4dab48caf7517e5623a3b540eef2ecc472cd62e2 GIT binary patch literal 138209 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 literal 0 HcmV?d00001 diff --git a/ingo/locale/de_DE/LC_MESSAGES/ingo.mo b/ingo/locale/de_DE/LC_MESSAGES/ingo.mo new file mode 100644 index 0000000000000000000000000000000000000000..aa9b3cedc7f83e081e13c5b9bd5a7d10b92a0fe4 GIT binary patch literal 173292 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 _= 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 +X|6u*~ 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 ZPpYDP1tKq_0U%UbrUb5Pb?IcS`hNq9OZNZ( literal 0 HcmV?d00001 diff --git a/ingo/locale/de_DE/help.xml b/ingo/locale/de_DE/help.xml new file mode 100644 index 000000000..157d1176e --- /dev/null +++ b/ingo/locale/de_DE/help.xml @@ -0,0 +1,294 @@ + + + + + Ausschlussliste + In der Ausschlussliste befinden sich alle Emailadressen, von denen + Sie wissen, dass diese Ihnen Nachrichten schicken, die Sie nicht in Ihrem + Postfach haben möchten. + + + Ausschlussliste: Aktion + Diese Aktion legt fest, was mit Nachrichten geschieht, die Sie von + einem blockierten Absender erhalten. Mögliche Aktionen sind + "Löschen" und "Verschieben". Wenn Sie "Löschen" auswählen, + wird die Nachricht gelöscht, bevor sie überhaupt ausgeliefert + wurde. Mit "Verschieben" wird die Nachricht in den angegebenen Ordner + ausgeliefert. + + + Ausschlussliste: Adressen + Die Liste der Adressen, die blockiert werden sollen. Jede Adresse + muss in einer eigenen Zeile eingetragen werden. + + + Regeln: Filterregeln + Dies ist die Hauptseite mit den verschiedenen Regeln. Hier + können Sie Regeln hinzufügen, indem Sie auf "Neue Regel" klicken. + Sie können Regeln arrangieren, indem Sie auf die Hoch- und + Runterpfeile in der "Verschieben"-Spalte klicken oder neue Regelpositionen + in die Eingabefelder eintragen, Regeln aktivieren und deaktivieren, indem + Sie auf das entsprechende Icon in der "Aktiviert"-Spalte klicken, und + einzelne Regeln bearbeiten, indem Sie auf das entsprechende Icon in der + "Bearbeiten"-Spalte oder auf den den Regelnamen klicken. + + Beachten Sie, dass die Regeln in der hier aufgelisteten Reihenfolge + ausgeführt werden. Wird zum Beispiel eine Email durch einen Eintrag + in der Ausschlussliste gelöscht, haben die nachfolgenden Regeln + keinen Einfluss auf diese Email mehr. + + + + + Weiterleitung + Damit können Sie Ihre eingehenden Emails automatisch an eine + oder mehrere andere Emailadressen weiterleiten. + + + Weiterleitung: Adressen + Hier können Sie festlegen, an welche Adressen Ihre Emails + weitergeleitet werden sollen. Sie können beliebig viele Emailadressen + eintragen, jede in einer eigenen Zeile. + + + Weiterleitung: Kopie behalten + Wenn Sie diese Einstellung aktivieren, werden Kopien aller + eingehenden Nachrichten in Ihrem Konto belassen. Unabhängig davon + werden alle Nachrichten an die Adressen, die Sie angegeben haben, + weitergeleitet. + + + Einstellungen: Detailierte Benachrichtigung anzeigen? + Wenn der IMAP-Filtertreiber verwendet wird, kann über diese + Einstellung festgelegt werden, wie ausführlich die Benachrichtigung + über die Auführung der Filter ausfällt. Wenn diese + Einstellung aktiviert wird, wird eine Benachrichtigung über jede + einzelne Nachricht, die gefiltert wurde, auf dem Bildschirm angezeigt. + Anderenfalls wird nur eine Zusammenfassung aller Filteraktionen + angezeigt. + + + Einstellungen: Nur (un)gelesene Nachrichten filtern? + Mit dieser Einstellung können Sie festlegen, welche Nachrichten + im Posteingang von den Filterregeln betroffen sein sollen. Sie können + die Filter entweder auf alle, nur auf gelesene, oder nur auf ungelesene + Nachrichten anwenden lassen. + + + Filterregel + Regeln sind der Grundbaustein Ihrer Emailfilter. Sie bestehen aus + einer oder mehrere Bedingungen und einer oder mehrere Aktionen. Wenn Sie + eine Nachricht erhalten, wird diese zunächst durch die Bedingungen + verarbeitet; wenn diese zutreffen, werden die Aktionen, die Sie angegeben + haben, mit dieser Nachricht ausgeführt. Filter können sehr + nützlich zur Aussortierung unerwünschter Emails sein, oder um Ihr + Emailkonto übersichtlicher zu gestalten, indem Sie + zusammengehörige Emails jeweils in unterschiedliche Ordner + einsortieren. + + + Filterregel: Aktion + Dies sind alle möglichen Aktionen, die ausgeführt werden + können, wenn eine eingehende Nachricht den Bedingungen entspricht, die + in der Filterregel angegeben wurde. Beachten Sie, dass eventuell nicht alle + diese Möglichkeiten zur Verfügung stehen, nur die Aktionen werden + angezeigt, die von Ihrem System unterstützt werden. + In meinen Posteingang ausliefern + Die Nachricht wird in Ihrem Posteingang gespeichert. Dies ist die + Standardeinstellung. + In diesen Ordner ausliefern + Die Nachricht wird in den angegebenen Ordner ausgeliefert. + Nachricht komplett löschen + Die Nachricht wird stillschweigend gelöscht. Weder Sie noch der + Absender erhalten eine Benachrichtigung über diesen Vorgang. + Weiterleiten an + Die Nachricht wird an die angegebene Emailadresse weitergeleitet. Sie + erhalten keine Kopie dieser Nachricht in Ihren Posteingang. + In meinen Posteingang ausliefern und weiterleiten an + Die Nachricht wird an die angegebene Emailadresse weitergeleitet, und + Sie erhalten eine Kopie dieser Nachricht in Ihren Posteingang. + Mit folgendem Grund ablehnen + Die Nachricht wird gelöscht, und der Absender dieser Nachricht + erhält eine Antwort mit dem Text, den Sie hier angeben. + + + Filterregel: Bedingungen kombinieren + Sie können mehrere Bedingungen in einer Regel angeben und diese + logisch mit "und" und "oder" verknüpfen. Komplexe Bedingungen mit + einer Kombination beider Verknüpfungstypen sind nicht + möglich. + Und + Wenn Sie "Alle der folgenden" auswählen, müssen eingehende + Nachrichten allen angegebenen Bedingungen entsprechen, bevor die + Filteraktionen ausgeführt werden. + Oder + Wenn Sie "Eine der folgenden" auswählen, müssen eingehende + Nachrichten mindestens einer der angegebenen Bedingungen entsprechen, bevor + die Filteraktionen ausgeführt werden. + + + Filterregel: Nachricht markieren + Sie können festlegen, dass eine Nachricht als Filteraktion mit + einer oder mehreren IMAP-Markierungen versehen wird. Die möglichen + Markierungen sind Gelesen, Markiert zur Wiedervorlage, Beantwortet und Gelöscht. + + + Filterregel: Bedingungen + Jede Bedingung einer Filterregel besteht aus drei Komponenten. Die + erste bestimmt das Feld der Nachricht, das überprüft werden soll. + Die zweite legt die Art der Überprüfung fest, und die dritte gibt + den Wert an, mit dem das Feld verglichen werden soll. Es gibt viele + verschiedene Vergleichsarten, die je nach dem verwendeten Filtersystem und + dem zu überprüfenden Nachrichtenfeld variieren können. Daher + werden von den folgenden Möglichkeiten nicht unbedingt alle + angezeigt. + Enhält + Ist wahr, wenn die angegebene Zeichenkette irgendwo im Feld gefunden + wird. Beispiel: max_mustermann@beispiel.de Enthält + mustermann@beispiel. + Enhält nicht + Ist wahr, wenn die angegebene Zeichenkette nicht im Feld gefunden + wird. Beispiel: max_mustermann@beispiel.de Enthält nicht + mustermann@beispiel. + Ist + Ist wahr, wenn die angegebene Zeichenkette dem Feld genau entspricht. + Beispiel: max_mustermann@beispiel.de Ist max_mustermann@beispiel.de. + Ist nicht + Ist wahr, wenn die angegebene Zeichenkette dem Feld nicht genau + entspricht. Beispiel: max_mustermann@beispiel Ist Nicht + max_mustermann@beispiel.de. + Beginnt mit + Ist wahr, wenn die angegebene Zeichenkette dem Anfang des Feldes + entspricht. Beispiel: max_mustermann@beispiel.de Beginnt mit + max_mustermann@. + Beginnt nicht mit + Ist wahr, wenn die angegebene Zeichenkette nicht dem Anfang des + Feldes entspricht. Beispiel: max_mustermann@beispiel.de Beginnt nicht mit + mustermann@beispiel.de. + Endet mit + Ist wahr, wenn die angegebene Zeichenkette dem Ende des Feldes + entspricht. Beispiel: max_mustermann@beispiel.de Endet mit + @beispiel.de. + Endet nicht mit + Ist wahr, wenn die angegebene Zeichenkette nicht dem Ende des Feldes + entspricht. Beispiel: max_mustermann@beispiel.de Endet nicht mit + max_mustermann@beispiel. + Existiert + Ist wahr, wenn das angegebene Feld in der Nachricht existiert, + unabhänging von dessen Inhalt. + Existiert nicht + Ist wahr, wenn das angegebene Feld nicht in der Nachricht + existiert. + Regulärer Ausdruck + Erlaubt die Verwendung komplexer, POSIX kompatibler, regulärer + Ausdrücke, um die Nachrichtenfelder zu überprüfen. Beispiel: + "Received from [*\.*\.*\.*] by (hosta|hostb).beispiel.de*" würde auf + "Received from [172.16.100.1] by hosta.beispiel.de on Tuesday" + zutreffen. + Entpricht (mit Platzhaltern) + "Entspricht" ist ähnlich zu "Enthält", erlaubt aber die + Verwendung von * und ? als Platzhalter. Ein * entspricht einer beliebigen + Anzahl von Zeichen, ? entspricht genau einem Zeichen. Beispiel: + "*benutzer?@beispiel.de" würde sowohl auf "benutzer1@beispiel.de" als + auch auf "andererbenutzer2@beispiel.de" zutreffen. + Entpricht nicht (mit Platzhaltern) + "Entspricht nicht" funktioniert genau wie "Entspricht", außer + dass die Bedingung falsch wird, wenn der angegebene Ausdruck dem Wert im + Nachrichtenfeld entspricht. + Kleiner als + Dies ist ein Vergleichstest, der den angegebenen Wert und den Wert im + Nachrichtenfeld numerisch vergleicht. + Kleiner als oder gleich + Dies ist ein Vergleichstest, der den angegebenen Wert und den Wert im + Nachrichtenfeld numerisch vergleicht. + Gleich + Dies ist ein Vergleichstest, der den angegebenen Wert und den Wert im + Nachrichtenfeld numerisch vergleicht. + Größer als oder gleich + Dies ist ein Vergleichstest, der den angegebenen Wert und den Wert im + Nachrichtenfeld numerisch vergleicht. + Größer als + Dies ist ein Vergleichstest, der den angegebenen Wert und den Wert im + Nachrichtenfeld numerisch vergleicht. + + + Filterregel: Regelname + Dies ist der Name der Regel, den Sie frei vergeben können. Er + wird in der Liste der Filterregeln benutzt, um auf die Regel + zuzugreifen. + + + Filterregel: Weitere Überprüfung anhalten + Wenn diese Einstellung ausgewählt ist und die Regel auf eine + Nachricht zutrifft, werden alle folgenden Regeln nicht mehr auf diese + Nachricht angewendet. + + + Abwesenheit + Abwesenheitsnachrichten sind automatische Antworten an Personen, die + Ihnen eine Email schreiben, während sie für eine längere + Zeit abwesend sind. + + + Abwesenheit: Dauer + Abwesenheitsnachrichten werden nur während des Zeitraumes + verschickt, den Sie angegeben haben. + + + Abwesenheit: Keine Antworten auf Nachrichten von Mailinglisten oder + Massenemails + Wenn Sie diese Einstellung aktivieren, werden keine + Abwesenheitsnachrichten als Anwort auf Nachrichten von Mailinglisten oder + solchen, die als Massenmails markiert sind, verschickt. + + + Abwesenheit: Antwortintervall + Hier können Sie die Anzahl der Tage angeben, die gewartet werden + soll, bevor ein Absender, der schon eine Abwesenheitsnachricht erhalten + hat, eine weitere zugeschickt bekommt. + + + Abwesenheit: Meine Emailadressen + Wenn Sie mehr als eine Emailadresse haben, für die Sie + Nachrichten in diesem Konto erhalten, können Sie diese hier + angeben. + + + Abwesenheit: Keine Antworten + An die angebenen Emailadressen werden keine Abwesenheitsnachrichten + verschickt. Jede Adresse muss in einer eigenen Zeile stehen. + + + Abwesenheit: Grund + Dieser Text wird in Ihren Abwesenheitsnachrichten verwendet. + + + Abwesenheit: Betreff + Dieser Betreff wird in Ihren Abwesenheitsnachrichten + verwendet. + + + Positivliste + In der Positivliste befinden sich alle Emailadressen, deren + Nachrichten Sie immer in Ihrem Postfach haben möchten. Jede Adresse + muss in einer eigenen Zeile stehen. + + + Positivliste: Adressen + Jede Adresse muss in einer eigenen Zeile stehen. + + + Spamfilter: Spam-Level + Das System wird Nachrichten mit einem Spam-Level größer oder gleich + diesem Wert als Spam behandeln. + Mit niedrigeren Werten werden mehr Nachrichten abgefangen, + allerdings mit dem dem Nachteil, dass die Wahrscheinlichkeit größer wird, + dass auch echte Nachrichten herausgefiltert werden. "5" ist ein typischer + Wert, falls auf Ihrem System SpamAssassin verwendet wird. + + + Spamfilter: Ordner für Spamnachrichten + Das System wird Nachrichten, die es als Spam klassifiziert, in diesen + Ordner verschieben. + + diff --git a/ingo/locale/el_GR/LC_MESSAGES/ingo.mo b/ingo/locale/el_GR/LC_MESSAGES/ingo.mo new file mode 100644 index 0000000000000000000000000000000000000000..b79fcc20143e764c8030d8cbbb1e2ffc03e1db3e GIT binary patch literal 136296 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