View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail;
20  
21  import davmail.ui.tray.DavGatewayTray;
22  import org.apache.log4j.ConsoleAppender;
23  import org.apache.log4j.FileAppender;
24  import org.apache.log4j.Level;
25  import org.apache.log4j.Logger;
26  import org.apache.log4j.PatternLayout;
27  import org.apache.log4j.RollingFileAppender;
28  
29  import java.io.BufferedReader;
30  import java.io.BufferedWriter;
31  import java.io.File;
32  import java.io.FileInputStream;
33  import java.io.FileOutputStream;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.InputStreamReader;
37  import java.io.OutputStreamWriter;
38  import java.nio.charset.StandardCharsets;
39  import java.nio.file.Files;
40  import java.nio.file.Path;
41  import java.nio.file.Paths;
42  import java.nio.file.attribute.FileAttribute;
43  import java.nio.file.attribute.PosixFilePermissions;
44  import java.util.ArrayList;
45  import java.util.Enumeration;
46  import java.util.Iterator;
47  import java.util.Map;
48  import java.util.Properties;
49  import java.util.TreeSet;
50  
51  import static org.apache.http.util.TextUtils.isEmpty;
52  
53  /**
54   * Settings facade.
55   * DavMail settings are stored in the .davmail.properties file in current
56   * user home directory or in the file specified on the command line.
57   */
58  public final class Settings {
59  
60      private static final Logger LOGGER = Logger.getLogger(Settings.class);
61  
62      public static final String OUTLOOK_URL = "https://outlook.office365.com";
63      public static final String O365_URL = OUTLOOK_URL + "/EWS/Exchange.asmx";
64  
65      public static final String GRAPH_URL = "https://graph.microsoft.com";
66  
67      public static final String O365_LOGIN_URL = "https://login.microsoftonline.com";
68  
69      // protocol modes
70      public static final String O365_EWS = "O365EWS"; // classic O365 over EWS
71      public static final String O365_GRAPH = "O365Graph"; // new O365 graph backend
72      public static final String EXCHANGE_EWS = "ExchangeEWS"; // on prem Exchange
73      public static final String EXCHANGE_WEBDAV = "ExchangeWebDav"; // on prem Exchange legacy webdav
74  
75      // authentication modes
76      public static final String O365_INTERACTIVE = "O365Interactive"; // interactive embedded browser O365 login
77      public static final String O365_MANUAL = "O365Manual"; // manual O365 authentication dialog
78      public static final String O365_DEVICECODE = "O365DeviceCode"; // O365 device code authentication
79      public static final String O365_TRANSPARENT = "O365Transparent"; // Transparent automated authentication
80  
81      // legacy protocol modes
82      public static final String O365_MODERN = "O365Modern";
83      public static final String O365 = "O365";
84      public static final String WEBDAV = "WebDav";
85      public static final String EWS = "EWS";
86      public static final String AUTO = "Auto";
87  
88      public static final String EDGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.49";
89  
90      private Settings() {
91      }
92  
93      private static final Properties SETTINGS_PROPERTIES = new Properties() {
94          @Override
95          public synchronized Enumeration<Object> keys() {
96              Enumeration<Object> keysEnumeration = super.keys();
97              TreeSet<String> sortedKeySet = new TreeSet<>();
98              while (keysEnumeration.hasMoreElements()) {
99                  sortedKeySet.add((String) keysEnumeration.nextElement());
100             }
101             final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
102             return new Enumeration<Object>() {
103 
104                 public boolean hasMoreElements() {
105                     return sortedKeysIterator.hasNext();
106                 }
107 
108                 public Object nextElement() {
109                     return sortedKeysIterator.next();
110                 }
111             };
112         }
113 
114     };
115     private static String configFilePath;
116     private static boolean isFirstStart;
117 
118     /**
119      * Set config file path (from command line parameter).
120      *
121      * @param path davmail properties file path
122      */
123     public static synchronized void setConfigFilePath(String path) {
124         configFilePath = path;
125     }
126 
127     /**
128      * Detect first launch (properties file does not exist).
129      *
130      * @return true if this is the first start with the current file path
131      */
132     public static synchronized boolean isFirstStart() {
133         return isFirstStart;
134     }
135 
136     /**
137      * Load properties from provided stream (used in webapp mode).
138      *
139      * @param inputStream properties stream
140      * @throws IOException on error
141      */
142     public static synchronized void load(InputStream inputStream) throws IOException {
143         SETTINGS_PROPERTIES.load(inputStream);
144         updateLoggingConfig();
145     }
146 
147     /**
148      * Load properties from current file path (command line or default).
149      */
150     public static synchronized void load() {
151         try {
152             if (configFilePath == null) {
153                 //noinspection AccessOfSystemProperties
154                 configFilePath = System.getProperty("user.home") + "/.davmail.properties";
155                 if (!new File(configFilePath).exists()) {
156                     // .davmail.properties does not exist, switch to new freedesktop compliant location
157                     configFilePath = getXDGConfigFilePath();
158                 }
159             }
160             File configFile = new File(configFilePath);
161             if (configFile.exists()) {
162                 try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
163                     load(fileInputStream);
164                 }
165             } else {
166                 isFirstStart = true;
167 
168                 // first start : set default values, ports above 1024 for unix/linux
169                 setDefaultSettings();
170                 save();
171             }
172         } catch (IOException e) {
173             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
174         }
175         updateLoggingConfig();
176     }
177 
178     /**
179      * Build an XDG compliant config file path.
180      *
181      * @return config file path
182      */
183     private static String getXDGConfigFilePath() {
184         String defaultConfigPath = System.getProperty("user.home")+".davmail.properties";
185         String xdgConfigDirectory = System.getenv("XDG_CONFIG_HOME");
186         if (xdgConfigDirectory == null) {
187             xdgConfigDirectory = System.getProperty("user.home")+"/.config";
188         }
189         File xdgConfigDirectoryFile = new File(xdgConfigDirectory);
190         if (!xdgConfigDirectoryFile.exists()) {
191             if (!xdgConfigDirectoryFile.mkdirs()) {
192                 return defaultConfigPath;
193             }
194         }
195         File xdgConfigDavMailDirectoryFile = new File(xdgConfigDirectory+"/davmail");
196         if (!xdgConfigDavMailDirectoryFile.exists()) {
197             if (!xdgConfigDavMailDirectoryFile.mkdirs()) {
198                 return defaultConfigPath;
199             }
200         }
201         return xdgConfigDirectory+"/davmail/davmail.properties";
202     }
203 
204     /**
205      * Set all settings to default values.
206      * Ports above 1024 for unix/linux
207      */
208     public static void setDefaultSettings() {
209         SETTINGS_PROPERTIES.put("davmail.mode", O365_EWS);
210         SETTINGS_PROPERTIES.put("davmail.authentication", O365_INTERACTIVE);
211         SETTINGS_PROPERTIES.put("davmail.url", getO365Url());
212         SETTINGS_PROPERTIES.put("davmail.popPort", "1110");
213         SETTINGS_PROPERTIES.put("davmail.imapPort", "1143");
214         SETTINGS_PROPERTIES.put("davmail.smtpPort", "1025");
215         SETTINGS_PROPERTIES.put("davmail.caldavPort", "1080");
216         SETTINGS_PROPERTIES.put("davmail.ldapPort", "1389");
217         SETTINGS_PROPERTIES.put("davmail.clientSoTimeout", "");
218         SETTINGS_PROPERTIES.put("davmail.keepDelay", "30");
219         SETTINGS_PROPERTIES.put("davmail.sentKeepDelay", "0");
220         SETTINGS_PROPERTIES.put("davmail.caldavPastDelay", "0");
221         SETTINGS_PROPERTIES.put("davmail.caldavAutoSchedule", Boolean.TRUE.toString());
222         SETTINGS_PROPERTIES.put("davmail.imapIdleDelay", "");
223         SETTINGS_PROPERTIES.put("davmail.folderSizeLimit", "");
224         SETTINGS_PROPERTIES.put("davmail.enableKeepAlive", Boolean.FALSE.toString());
225         SETTINGS_PROPERTIES.put("davmail.allowRemote", Boolean.FALSE.toString());
226         SETTINGS_PROPERTIES.put("davmail.bindAddress", "");
227         SETTINGS_PROPERTIES.put("davmail.useSystemProxies", Boolean.FALSE.toString());
228         SETTINGS_PROPERTIES.put("davmail.enableProxy", Boolean.FALSE.toString());
229         SETTINGS_PROPERTIES.put("davmail.enableKerberos", "false");
230         SETTINGS_PROPERTIES.put("davmail.disableUpdateCheck", "false");
231         SETTINGS_PROPERTIES.put("davmail.proxyHost", "");
232         SETTINGS_PROPERTIES.put("davmail.proxyPort", "");
233         SETTINGS_PROPERTIES.put("davmail.proxyUser", "");
234         SETTINGS_PROPERTIES.put("davmail.proxyPassword", "");
235         SETTINGS_PROPERTIES.put("davmail.noProxyFor", "");
236         SETTINGS_PROPERTIES.put("davmail.server", Boolean.FALSE.toString());
237         SETTINGS_PROPERTIES.put("davmail.server.certificate.hash", "");
238         SETTINGS_PROPERTIES.put("davmail.caldavAlarmSound", "");
239         SETTINGS_PROPERTIES.put("davmail.carddavReadPhoto", Boolean.TRUE.toString());
240         SETTINGS_PROPERTIES.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
241         if (isLinux()) {
242             SETTINGS_PROPERTIES.put("davmail.enableTray", Boolean.FALSE.toString());
243         } else {
244             SETTINGS_PROPERTIES.put("davmail.enableTray", Boolean.TRUE.toString());
245         }
246         SETTINGS_PROPERTIES.put("davmail.showStartupBanner", Boolean.TRUE.toString());
247         SETTINGS_PROPERTIES.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
248         SETTINGS_PROPERTIES.put("davmail.disableTrayActivitySwitch", Boolean.FALSE.toString());
249         SETTINGS_PROPERTIES.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
250         SETTINGS_PROPERTIES.put("davmail.imapAlwaysApproxMsgSize", Boolean.FALSE.toString());
251         SETTINGS_PROPERTIES.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
252         SETTINGS_PROPERTIES.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
253         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreType", "");
254         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreFile", "");
255         SETTINGS_PROPERTIES.put("davmail.ssl.keystorePass", "");
256         SETTINGS_PROPERTIES.put("davmail.ssl.keyPass", "");
257         if (isWindows()) {
258             // default to MSCAPI on Windows for native client certificate access
259             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "MSCAPI");
260         } else {
261             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "");
262         }
263         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreFile", "");
264         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystorePass", "");
265         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Library", "");
266         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Config", "");
267         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
268         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
269         SETTINGS_PROPERTIES.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
270         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
271         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
272 
273         // logging
274         SETTINGS_PROPERTIES.put("log4j.rootLogger", Level.WARN.toString());
275         SETTINGS_PROPERTIES.put("log4j.logger.davmail", Level.DEBUG.toString());
276         SETTINGS_PROPERTIES.put("log4j.logger.httpclient.wire", Level.WARN.toString());
277         SETTINGS_PROPERTIES.put("log4j.logger.httpclient", Level.WARN.toString());
278         String logFilePath = "";
279         if (isFlatpak()) {
280             logFilePath = System.getenv("XDG_DATA_HOME")+"/davmail.log";
281         }
282         SETTINGS_PROPERTIES.put("davmail.logFilePath", logFilePath);
283     }
284 
285     /**
286      * Return DavMail log file path
287      *
288      * @return full log file path
289      */
290     public static String getLogFilePath() {
291         String logFilePath = Settings.getProperty("davmail.logFilePath");
292         // set default log file path
293         if ((logFilePath == null || logFilePath.isEmpty())) {
294             if (Settings.getBooleanProperty("davmail.server")) {
295                 logFilePath = "davmail.log";
296             } else if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
297                 // store davmail.log in OSX Logs directory
298                 logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
299             } else {
300                 // store davmail.log in user home folder
301                 logFilePath = System.getProperty("user.home") + "/davmail.log";
302             }
303         } else {
304             File logFile = new File(logFilePath);
305             if (logFile.isDirectory()) {
306                 logFilePath += "/davmail.log";
307             }
308         }
309         return logFilePath;
310     }
311 
312     /**
313      * Return DavMail log file directory
314      *
315      * @return full log file directory
316      */
317     public static String getLogFileDirectory() {
318         String logFilePath = getLogFilePath();
319         if (logFilePath == null || logFilePath.isEmpty()) {
320             return ".";
321         }
322         int lastSlashIndex = logFilePath.lastIndexOf('/');
323         if (lastSlashIndex == -1) {
324             lastSlashIndex = logFilePath.lastIndexOf('\\');
325         }
326         if (lastSlashIndex >= 0) {
327             return logFilePath.substring(0, lastSlashIndex);
328         } else {
329             return ".";
330         }
331     }
332 
333     /**
334      * Update Log4J config from settings.
335      */
336     public static void updateLoggingConfig() {
337         String logFilePath = getLogFilePath();
338 
339         try {
340             if (!isDocker()) {
341                 if (logFilePath != null && !logFilePath.isEmpty()) {
342                     File logFile = new File(logFilePath);
343                     // create parent directory if needed
344                     File logFileDir = logFile.getParentFile();
345                     if (logFileDir != null && !logFileDir.exists() && (!logFileDir.mkdirs())) {
346                         DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
347                         throw new IOException();
348 
349                     }
350                 } else {
351                     logFilePath = "davmail.log";
352                 }
353 
354                 synchronized (Logger.getRootLogger()) {
355                     // Build file appender
356                     FileAppender fileAppender = (FileAppender) Logger.getRootLogger().getAppender("FileAppender");
357                     if (fileAppender == null) {
358                         String logFileSize = Settings.getProperty("davmail.logFileSize");
359                         if (logFileSize == null || logFileSize.isEmpty()) {
360                             logFileSize = "1MB";
361                         }
362                         // set log file size to 0 to use an external rotation mechanism, e.g. logrotate
363                         if ("0".equals(logFileSize)) {
364                             fileAppender = new FileAppender();
365                         } else {
366                             fileAppender = new RollingFileAppender();
367                             ((RollingFileAppender) fileAppender).setMaxBackupIndex(2);
368                             ((RollingFileAppender) fileAppender).setMaxFileSize(logFileSize);
369                         }
370                         fileAppender.setName("FileAppender");
371                         fileAppender.setEncoding("UTF-8");
372                         fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
373                     }
374                     fileAppender.setFile(logFilePath, true, false, 8192);
375                     Logger.getRootLogger().addAppender(fileAppender);
376                 }
377             }
378 
379             // disable ConsoleAppender in gui mode
380             ConsoleAppender consoleAppender = (ConsoleAppender) Logger.getRootLogger().getAppender("ConsoleAppender");
381             if (consoleAppender != null) {
382                 consoleAppender.setThreshold(Level.ALL);
383             }
384 
385         } catch (IOException e) {
386             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
387         }
388 
389         // update logging levels
390         Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
391         Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
392         // set logging levels for HttpClient 4
393         Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
394         Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
395         Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
396     }
397 
398     /**
399      * Save settings in current file path (command line or default).
400      */
401     public static synchronized void save() {
402         // configFilePath is null in some test cases
403         if (configFilePath != null) {
404             // clone settings
405             Properties properties = new Properties();
406             properties.putAll(SETTINGS_PROPERTIES);
407             // file lines
408             ArrayList<String> lines = new ArrayList<>();
409 
410             // try to make .davmail.properties file readable by user only on create
411             Path path = Paths.get(configFilePath);
412             if (!Files.exists(path) && isUnix()) {
413                 FileAttribute<?> permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
414                 try {
415                     Files.createFile(path, permissions);
416                 } catch (IOException e) {
417                     LOGGER.error(e.getMessage());
418                 }
419             }
420 
421             readLines(lines, properties);
422 
423             try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.ISO_8859_1))) {
424                 for (String value : lines) {
425                     writer.write(value);
426                     writer.newLine();
427                 }
428 
429                 // write remaining lines
430                 Enumeration<?> propertyEnumeration = properties.propertyNames();
431                 while (propertyEnumeration.hasMoreElements()) {
432                     String propertyName = (String) propertyEnumeration.nextElement();
433                     writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
434                     writer.newLine();
435                 }
436             } catch (IOException e) {
437                 DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
438             }
439         }
440         updateLoggingConfig();
441     }
442 
443     private static void readLines(ArrayList<String> lines, Properties properties) {
444         try {
445             File configFile = new File(configFilePath);
446             if (configFile.exists()) {
447                 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(configFile.toPath()), StandardCharsets.ISO_8859_1))) {
448                     String line;
449                     while ((line = reader.readLine()) != null) {
450                         lines.add(convertLine(line, properties));
451                     }
452                 }
453             }
454         } catch (IOException e) {
455             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
456         }
457     }
458 
459     /**
460      * Convert input property line to new line with value from properties.
461      * Preserve comments
462      *
463      * @param line       input line
464      * @param properties new property values
465      * @return new line
466      */
467     private static String convertLine(String line, Properties properties) {
468         int hashIndex = line.indexOf('#');
469         int equalsIndex = line.indexOf('=');
470         // allow # in values, no a comment
471         // comments are pass through
472         if (equalsIndex >= 0 && (hashIndex < 0 || hashIndex >= equalsIndex)) {
473             String key = line.substring(0, equalsIndex);
474             String value = properties.getProperty(key);
475             if (value != null) {
476                 // build property with new value
477                 line = key + "=" + escapeValue(value);
478                 // remove property from source
479                 properties.remove(key);
480             }
481         }
482         return line;
483     }
484 
485     /**
486      * Escape backslash in value.
487      *
488      * @param value value
489      * @return escaped value
490      */
491     private static String escapeValue(String value) {
492         StringBuilder buffer = new StringBuilder();
493         for (char c : value.toCharArray()) {
494             if (c == '\\') {
495                 buffer.append('\\');
496             }
497             buffer.append(c);
498         }
499         return buffer.toString();
500     }
501 
502 
503     /**
504      * Get a property value as String.
505      *
506      * @param property property name
507      * @return property value
508      */
509     public static synchronized String getProperty(String property) {
510         String value = SETTINGS_PROPERTIES.getProperty(property);
511         // return null on empty value
512         if (value != null && value.isEmpty()) {
513             value = null;
514         }
515         return value;
516     }
517 
518     /**
519      * Get property value or default.
520      *
521      * @param property     property name
522      * @param defaultValue default property value
523      * @return property value
524      */
525     public static synchronized String getProperty(String property, String defaultValue) {
526         String value = getProperty(property);
527         if (value == null) {
528             value = defaultValue;
529         }
530         return value;
531     }
532 
533 
534     /**
535      * Get a property value as char[].
536      *
537      * @param property property name
538      * @return property value
539      */
540     public static synchronized char[] getCharArrayProperty(String property) {
541         String propertyValue = Settings.getProperty(property);
542         char[] value = null;
543         if (propertyValue != null) {
544             value = propertyValue.toCharArray();
545         }
546         return value;
547     }
548 
549     /**
550      * Set a property value.
551      *
552      * @param property property name
553      * @param value    property value
554      */
555     public static synchronized void setProperty(String property, String value) {
556         if (value != null) {
557             SETTINGS_PROPERTIES.setProperty(property, value);
558         } else {
559             SETTINGS_PROPERTIES.setProperty(property, "");
560         }
561     }
562 
563     /**
564      * Get a property value as int.
565      *
566      * @param property property name
567      * @return property value
568      */
569     public static synchronized int getIntProperty(String property) {
570         return getIntProperty(property, 0);
571     }
572 
573     /**
574      * Get a property value as int, return default value if null.
575      *
576      * @param property     property name
577      * @param defaultValue default property value
578      * @return property value
579      */
580     public static synchronized int getIntProperty(String property, int defaultValue) {
581         int value = defaultValue;
582         try {
583             String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
584             if (propertyValue != null && !propertyValue.isEmpty()) {
585                 value = Integer.parseInt(propertyValue);
586             }
587         } catch (NumberFormatException e) {
588             DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
589         }
590         return value;
591     }
592 
593     /**
594      * Get a property value as boolean.
595      *
596      * @param property property name
597      * @return property value
598      */
599     public static synchronized boolean getBooleanProperty(String property) {
600         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
601         return Boolean.parseBoolean(propertyValue);
602     }
603 
604     /**
605      * Get a property value as boolean.
606      *
607      * @param property     property name
608      * @param defaultValue default property value
609      * @return property value
610      */
611     public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
612         boolean value = defaultValue;
613         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
614         if (propertyValue != null && !propertyValue.isEmpty()) {
615             value = Boolean.parseBoolean(propertyValue);
616         }
617         return value;
618     }
619 
620     public static synchronized String loadRefreshToken(String username) {
621         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
622         if (isEmpty(tokenFilePath)) {
623             return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
624         } else {
625             return loadTokenFromFile(tokenFilePath, username.toLowerCase());
626         }
627     }
628 
629 
630     public static synchronized void storeRefreshToken(String username, String refreshToken) {
631         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
632         if (isEmpty(tokenFilePath)) {
633             Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
634             Settings.save();
635         } else {
636             saveTokenToFile(tokenFilePath, username.toLowerCase(), refreshToken);
637         }
638     }
639 
640     /**
641      * Persist token in davmail.oauth.tokenFilePath.
642      *
643      * @param tokenFilePath token file path
644      * @param username      username
645      * @param refreshToken  Oauth refresh token
646      */
647     private static void saveTokenToFile(String tokenFilePath, String username, String refreshToken) {
648         try {
649             checkCreateTokenFilePath(tokenFilePath);
650             Properties properties = new Properties();
651             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
652                 properties.load(fis);
653             }
654             properties.setProperty(username, refreshToken);
655             try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
656                 properties.store(fos, "Oauth tokens");
657             }
658         } catch (IOException e) {
659             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
660         }
661     }
662 
663     /**
664      * Load token from davmail.oauth.tokenFilePath.
665      *
666      * @param tokenFilePath token file path
667      * @param username      username
668      * @return encrypted token value
669      */
670     private static String loadTokenFromFile(String tokenFilePath, String username) {
671         try {
672             checkCreateTokenFilePath(tokenFilePath);
673             Properties properties = new Properties();
674             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
675                 properties.load(fis);
676             }
677             return properties.getProperty(username);
678         } catch (IOException e) {
679             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
680         }
681         return null;
682     }
683 
684     private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
685         File file = new File(tokenFilePath);
686         File parentFile = file.getParentFile();
687         if (parentFile != null && (parentFile.mkdirs())) {
688             LOGGER.info("Created token file directory " + parentFile.getAbsolutePath());
689 
690         }
691         if (file.createNewFile()) {
692             LOGGER.info("Created token file " + tokenFilePath);
693         }
694     }
695 
696     /**
697      * Build logging properties prefix.
698      *
699      * @param category logging category
700      * @return prefix
701      */
702     private static String getLoggingPrefix(String category) {
703         String prefix;
704         if ("rootLogger".equals(category)) {
705             prefix = "log4j.";
706         } else {
707             prefix = "log4j.logger.";
708         }
709         return prefix;
710     }
711 
712     /**
713      * Return Log4J logging level for the category.
714      *
715      * @param category logging category
716      * @return logging level
717      */
718     public static synchronized Level getLoggingLevel(String category) {
719         String prefix = getLoggingPrefix(category);
720         String currentValue = SETTINGS_PROPERTIES.getProperty(prefix + category);
721 
722         if (currentValue != null && !currentValue.isEmpty()) {
723             return Level.toLevel(currentValue);
724         } else if ("rootLogger".equals(category)) {
725             return Logger.getRootLogger().getLevel();
726         } else {
727             return Logger.getLogger(category).getLevel();
728         }
729     }
730 
731     /**
732      * Get all properties that are in the specified scope, that is, that start with '&lt;scope&gt;.'.
733      *
734      * @param scope start of property name
735      * @return properties
736      */
737     public static synchronized Properties getSubProperties(String scope) {
738         final String keyStart;
739         if (scope == null || scope.isEmpty()) {
740             keyStart = "";
741         } else if (scope.endsWith(".")) {
742             keyStart = scope;
743         } else {
744             keyStart = scope + '.';
745         }
746         Properties result = new Properties();
747         for (Map.Entry<Object, Object> entry : SETTINGS_PROPERTIES.entrySet()) {
748             String key = (String) entry.getKey();
749             if (key.startsWith(keyStart)) {
750                 String value = (String) entry.getValue();
751                 result.setProperty(key.substring(keyStart.length()), value);
752             }
753         }
754         return result;
755     }
756 
757     /**
758      * Set Log4J logging level for the category
759      *
760      * @param category logging category
761      * @param level    logging level
762      */
763     public static synchronized void setLoggingLevel(String category, Level level) {
764         if (level != null) {
765             String prefix = getLoggingPrefix(category);
766             SETTINGS_PROPERTIES.setProperty(prefix + category, level.toString());
767             if ("rootLogger".equals(category)) {
768                 Logger.getRootLogger().setLevel(level);
769             } else {
770                 Logger.getLogger(category).setLevel(level);
771             }
772         }
773     }
774 
775     /**
776      * Change and save a single property.
777      *
778      * @param property property name
779      * @param value    property value
780      */
781     public static synchronized void saveProperty(String property, String value) {
782         Settings.load();
783         Settings.setProperty(property, value);
784         Settings.save();
785     }
786 
787     /**
788      * Test if running on Windows
789      *
790      * @return true on Windows
791      */
792     public static boolean isWindows() {
793         return System.getProperty("os.name").toLowerCase().startsWith("windows");
794     }
795 
796     /**
797      * Test if running on OSX
798      *
799      * @return true on Mac OS X
800      */
801     public static boolean isOSX() {
802         return System.getProperty("os.name").toLowerCase().startsWith("mac os x");
803     }
804 
805     /**
806      * Test if running on Linux
807      *
808      * @return true on Linux
809      */
810     public static boolean isLinux() {
811         return System.getProperty("os.name").toLowerCase().startsWith("linux");
812     }
813 
814     public static boolean isUnix() {
815         return isLinux() ||
816                 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
817     }
818 
819     public static String getUserAgent() {
820         return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
821     }
822 
823     public static String getOutlookUrl() {
824         String tld = getProperty("davmail.tld");
825         String outlookUrl = getProperty("davmail.outlookUrl");
826         if (outlookUrl != null) {
827             return outlookUrl;
828         } else if (tld == null) {
829             return OUTLOOK_URL;
830         } else {
831             return "https://outlook.office365." + tld;
832         }
833     }
834 
835     public static String getO365Url() {
836         String tld = getProperty("davmail.tld");
837         String outlookUrl = getProperty("davmail.outlookUrl");
838         if (outlookUrl != null) {
839             return outlookUrl + "/EWS/Exchange.asmx";
840         } else if (tld == null) {
841             return O365_URL;
842         } else {
843             return "https://outlook.office365." + tld + "/EWS/Exchange.asmx";
844         }
845     }
846 
847     /**
848      * Handle custom graph endpoints.
849      * See <a href="https://learn.microsoft.com/en-us/graph/deployments">...</a>
850      * @return graph endpoint url
851      */
852     public static String getGraphUrl() {
853         String tld = getProperty("davmail.tld");
854         String graphUrl = getProperty("davmail.graphUrl");
855         if (graphUrl != null) {
856             return graphUrl;
857         } else if (tld == null) {
858             return GRAPH_URL;
859         } else {
860             return "https://graph.microsoft." + tld;
861         }
862     }
863 
864     public static String getO365LoginUrl() {
865         String tld = getProperty("davmail.tld");
866         String loginUrl = getProperty("davmail.loginUrl");
867         if (loginUrl != null) {
868             return loginUrl;
869         } else if (tld == null) {
870             return O365_LOGIN_URL;
871         } else {
872             return "https://login.microsoftonline." + tld;
873         }
874     }
875 
876     public static String getOauthScope() {
877         String defaultOauthScope = "openid profile offline_access Mail.ReadWrite Calendars.ReadWrite MailboxSettings.Read Mail.ReadWrite.Shared Contacts.ReadWrite Tasks.ReadWrite Mail.Send People.Read";
878         if (Settings.getProperty("davmail.oauth.redirectUri","").startsWith("urn:")) {
879             defaultOauthScope = "openid profile offline_access";
880         }
881         return Settings.getProperty("davmail.oauth.scope", defaultOauthScope);
882     }
883 
884     public static boolean isGraphEnabled() {
885         return Settings.O365_GRAPH.equals(Settings.getProperty("davmail.mode")) || Settings.getBooleanProperty("davmail.enableGraph");
886     }
887 
888     public static boolean isSWTAvailable() {
889         boolean isSWTAvailable = false;
890         ClassLoader classloader = Settings.class.getClassLoader();
891         try {
892             // trigger ClassNotFoundException
893             classloader.loadClass("org.eclipse.swt.SWT");
894             isSWTAvailable = true;
895         } catch (Throwable e) {
896             // do not log on OSX, using standard system tray by default
897             if (!isOSX()) {
898                 LOGGER.info(new BundleMessage("LOG_SWT_NOT_AVAILABLE"));
899             }
900         }
901         return isSWTAvailable;
902     }
903 
904     public static boolean isJFXAvailable() {
905         boolean isJFXAvailable = false;
906         try {
907             Class.forName("javafx.application.Platform");
908             isJFXAvailable = true;
909         } catch (ClassNotFoundException | NullPointerException e) {
910             LOGGER.info("JavaFX (OpenJFX) not available");
911         }
912         return isJFXAvailable;
913     }
914 
915     public static boolean isDocker() {
916         boolean isDocker = new File("/.dockerenv").exists();
917         boolean isPodman = new File("/run/.containerenv").exists();
918         if (isDocker) {
919             LOGGER.info("Running in docker or podman");
920         }
921         return isDocker || isPodman;
922     }
923 
924     public static boolean isFlatpak() {
925         boolean isFlatpak = "flatpak".equals(System.getenv("container"));
926         if (isFlatpak) {
927             LOGGER.info("Running in Flatpak");
928         }
929         return isFlatpak;
930     }
931 
932     /**
933      * Set davmail properties path in Docker and Flatpak
934      * @return davmail.properties path
935      */
936     public static synchronized String getConfigFilePath() {
937         if (isFlatpak()) {
938             return System.getenv("XDG_CONFIG_HOME")+"/davmail.properties";
939         } else {
940             // Docker
941             return System.getenv("DAVMAIL_PROPERTIES");
942         }
943     }
944 }