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      public static final String O365 = "O365";
70      public static final String O365_MODERN = "O365Modern";
71      public static final String O365_INTERACTIVE = "O365Interactive";
72      public static final String O365_MANUAL = "O365Manual";
73      public static final String WEBDAV = "WebDav";
74      public static final String EWS = "EWS";
75      public static final String AUTO = "Auto";
76  
77      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";
78  
79      private Settings() {
80      }
81  
82      private static final Properties SETTINGS_PROPERTIES = new Properties() {
83          @Override
84          public synchronized Enumeration<Object> keys() {
85              Enumeration<Object> keysEnumeration = super.keys();
86              TreeSet<String> sortedKeySet = new TreeSet<>();
87              while (keysEnumeration.hasMoreElements()) {
88                  sortedKeySet.add((String) keysEnumeration.nextElement());
89              }
90              final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
91              return new Enumeration<Object>() {
92  
93                  public boolean hasMoreElements() {
94                      return sortedKeysIterator.hasNext();
95                  }
96  
97                  public Object nextElement() {
98                      return sortedKeysIterator.next();
99                  }
100             };
101         }
102 
103     };
104     private static String configFilePath;
105     private static boolean isFirstStart;
106 
107     /**
108      * Set config file path (from command line parameter).
109      *
110      * @param path davmail properties file path
111      */
112     public static synchronized void setConfigFilePath(String path) {
113         configFilePath = path;
114     }
115 
116     /**
117      * Detect first launch (properties file does not exist).
118      *
119      * @return true if this is the first start with the current file path
120      */
121     public static synchronized boolean isFirstStart() {
122         return isFirstStart;
123     }
124 
125     /**
126      * Load properties from provided stream (used in webapp mode).
127      *
128      * @param inputStream properties stream
129      * @throws IOException on error
130      */
131     public static synchronized void load(InputStream inputStream) throws IOException {
132         SETTINGS_PROPERTIES.load(inputStream);
133         updateLoggingConfig();
134     }
135 
136     /**
137      * Load properties from current file path (command line or default).
138      */
139     public static synchronized void load() {
140         try {
141             if (configFilePath == null) {
142                 //noinspection AccessOfSystemProperties
143                 configFilePath = System.getProperty("user.home") + "/.davmail.properties";
144             }
145             File configFile = new File(configFilePath);
146             if (configFile.exists()) {
147                 try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
148                     load(fileInputStream);
149                 }
150             } else {
151                 isFirstStart = true;
152 
153                 // first start : set default values, ports above 1024 for unix/linux
154                 setDefaultSettings();
155                 save();
156             }
157         } catch (IOException e) {
158             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
159         }
160         updateLoggingConfig();
161     }
162 
163     /**
164      * Set all settings to default values.
165      * Ports above 1024 for unix/linux
166      */
167     public static void setDefaultSettings() {
168         SETTINGS_PROPERTIES.put("davmail.mode", O365_INTERACTIVE);
169         SETTINGS_PROPERTIES.put("davmail.url", getO365Url());
170         SETTINGS_PROPERTIES.put("davmail.popPort", "1110");
171         SETTINGS_PROPERTIES.put("davmail.imapPort", "1143");
172         SETTINGS_PROPERTIES.put("davmail.smtpPort", "1025");
173         SETTINGS_PROPERTIES.put("davmail.caldavPort", "1080");
174         SETTINGS_PROPERTIES.put("davmail.ldapPort", "1389");
175         SETTINGS_PROPERTIES.put("davmail.clientSoTimeout", "");
176         SETTINGS_PROPERTIES.put("davmail.keepDelay", "30");
177         SETTINGS_PROPERTIES.put("davmail.sentKeepDelay", "0");
178         SETTINGS_PROPERTIES.put("davmail.caldavPastDelay", "0");
179         SETTINGS_PROPERTIES.put("davmail.caldavAutoSchedule", Boolean.TRUE.toString());
180         SETTINGS_PROPERTIES.put("davmail.imapIdleDelay", "");
181         SETTINGS_PROPERTIES.put("davmail.folderSizeLimit", "");
182         SETTINGS_PROPERTIES.put("davmail.enableKeepAlive", Boolean.FALSE.toString());
183         SETTINGS_PROPERTIES.put("davmail.allowRemote", Boolean.FALSE.toString());
184         SETTINGS_PROPERTIES.put("davmail.bindAddress", "");
185         SETTINGS_PROPERTIES.put("davmail.useSystemProxies", Boolean.FALSE.toString());
186         SETTINGS_PROPERTIES.put("davmail.enableProxy", Boolean.FALSE.toString());
187         SETTINGS_PROPERTIES.put("davmail.enableKerberos", "false");
188         SETTINGS_PROPERTIES.put("davmail.disableUpdateCheck", "false");
189         SETTINGS_PROPERTIES.put("davmail.proxyHost", "");
190         SETTINGS_PROPERTIES.put("davmail.proxyPort", "");
191         SETTINGS_PROPERTIES.put("davmail.proxyUser", "");
192         SETTINGS_PROPERTIES.put("davmail.proxyPassword", "");
193         SETTINGS_PROPERTIES.put("davmail.noProxyFor", "");
194         SETTINGS_PROPERTIES.put("davmail.server", Boolean.FALSE.toString());
195         SETTINGS_PROPERTIES.put("davmail.server.certificate.hash", "");
196         SETTINGS_PROPERTIES.put("davmail.caldavAlarmSound", "");
197         SETTINGS_PROPERTIES.put("davmail.carddavReadPhoto", Boolean.TRUE.toString());
198         SETTINGS_PROPERTIES.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
199         SETTINGS_PROPERTIES.put("davmail.showStartupBanner", Boolean.TRUE.toString());
200         SETTINGS_PROPERTIES.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
201         SETTINGS_PROPERTIES.put("davmail.disableTrayActivitySwitch", Boolean.FALSE.toString());
202         SETTINGS_PROPERTIES.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
203         SETTINGS_PROPERTIES.put("davmail.imapAlwaysApproxMsgSize", Boolean.FALSE.toString());
204         SETTINGS_PROPERTIES.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
205         SETTINGS_PROPERTIES.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
206         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreType", "");
207         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreFile", "");
208         SETTINGS_PROPERTIES.put("davmail.ssl.keystorePass", "");
209         SETTINGS_PROPERTIES.put("davmail.ssl.keyPass", "");
210         if (isWindows()) {
211             // default to MSCAPI on windows for native client certificate access
212             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "MSCAPI");
213         } else {
214             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "");
215         }
216         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreFile", "");
217         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystorePass", "");
218         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Library", "");
219         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Config", "");
220         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
221         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
222         SETTINGS_PROPERTIES.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
223         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
224         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
225 
226         // logging
227         SETTINGS_PROPERTIES.put("log4j.rootLogger", Level.WARN.toString());
228         SETTINGS_PROPERTIES.put("log4j.logger.davmail", Level.DEBUG.toString());
229         SETTINGS_PROPERTIES.put("log4j.logger.httpclient.wire", Level.WARN.toString());
230         SETTINGS_PROPERTIES.put("log4j.logger.httpclient", Level.WARN.toString());
231         SETTINGS_PROPERTIES.put("davmail.logFilePath", "");
232     }
233 
234     /**
235      * Return DavMail log file path
236      *
237      * @return full log file path
238      */
239     public static String getLogFilePath() {
240         String logFilePath = Settings.getProperty("davmail.logFilePath");
241         // set default log file path
242         if ((logFilePath == null || logFilePath.isEmpty())) {
243             if (Settings.getBooleanProperty("davmail.server")) {
244                 logFilePath = "davmail.log";
245             } else if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
246                 // store davmail.log in OSX Logs directory
247                 logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
248             } else {
249                 // store davmail.log in user home folder
250                 logFilePath = System.getProperty("user.home") + "/davmail.log";
251             }
252         } else {
253             File logFile = new File(logFilePath);
254             if (logFile.isDirectory()) {
255                 logFilePath += "/davmail.log";
256             }
257         }
258         return logFilePath;
259     }
260 
261     /**
262      * Return DavMail log file directory
263      *
264      * @return full log file directory
265      */
266     public static String getLogFileDirectory() {
267         String logFilePath = getLogFilePath();
268         if (logFilePath == null || logFilePath.isEmpty()) {
269             return ".";
270         }
271         int lastSlashIndex = logFilePath.lastIndexOf('/');
272         if (lastSlashIndex == -1) {
273             lastSlashIndex = logFilePath.lastIndexOf('\\');
274         }
275         if (lastSlashIndex >= 0) {
276             return logFilePath.substring(0, lastSlashIndex);
277         } else {
278             return ".";
279         }
280     }
281 
282     /**
283      * Update Log4J config from settings.
284      */
285     public static void updateLoggingConfig() {
286         String logFilePath = getLogFilePath();
287 
288         try {
289             if (!isDocker()) {
290                 if (logFilePath != null && !logFilePath.isEmpty()) {
291                     File logFile = new File(logFilePath);
292                     // create parent directory if needed
293                     File logFileDir = logFile.getParentFile();
294                     if (logFileDir != null && !logFileDir.exists() && (!logFileDir.mkdirs())) {
295                         DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
296                         throw new IOException();
297 
298                     }
299                 } else {
300                     logFilePath = "davmail.log";
301                 }
302 
303                 synchronized (Logger.getRootLogger()) {
304                     // Build file appender
305                     FileAppender fileAppender = (FileAppender) Logger.getRootLogger().getAppender("FileAppender");
306                     if (fileAppender == null) {
307                         String logFileSize = Settings.getProperty("davmail.logFileSize");
308                         if (logFileSize == null || logFileSize.isEmpty()) {
309                             logFileSize = "1MB";
310                         }
311                         // set log file size to 0 to use an external rotation mechanism, e.g. logrotate
312                         if ("0".equals(logFileSize)) {
313                             fileAppender = new FileAppender();
314                         } else {
315                             fileAppender = new RollingFileAppender();
316                             ((RollingFileAppender) fileAppender).setMaxBackupIndex(2);
317                             ((RollingFileAppender) fileAppender).setMaxFileSize(logFileSize);
318                         }
319                         fileAppender.setName("FileAppender");
320                         fileAppender.setEncoding("UTF-8");
321                         fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
322                     }
323                     fileAppender.setFile(logFilePath, true, false, 8192);
324                     Logger.getRootLogger().addAppender(fileAppender);
325                 }
326             }
327 
328             // disable ConsoleAppender in gui mode
329             ConsoleAppender consoleAppender = (ConsoleAppender) Logger.getRootLogger().getAppender("ConsoleAppender");
330             if (consoleAppender != null) {
331                 if (Settings.getBooleanProperty("davmail.server")) {
332                     consoleAppender.setThreshold(Level.ALL);
333                 } else {
334                     consoleAppender.setThreshold(Level.INFO);
335                 }
336             }
337 
338         } catch (IOException e) {
339             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
340         }
341 
342         // update logging levels
343         Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
344         Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
345         // set logging levels for HttpClient 4
346         Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
347         Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
348         Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
349     }
350 
351     /**
352      * Save settings in current file path (command line or default).
353      */
354     public static synchronized void save() {
355         // configFilePath is null in some test cases
356         if (configFilePath != null) {
357             // clone settings
358             Properties properties = new Properties();
359             properties.putAll(SETTINGS_PROPERTIES);
360             // file lines
361             ArrayList<String> lines = new ArrayList<>();
362 
363             // try to make .davmail.properties file readable by user only on create
364             Path path = Paths.get(configFilePath);
365             if (!Files.exists(path) && isUnix()) {
366                 FileAttribute<?> permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
367                 try {
368                     Files.createFile(path, permissions);
369                 } catch (IOException e) {
370                     LOGGER.error(e.getMessage());
371                 }
372             }
373 
374             readLines(lines, properties);
375 
376             try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.ISO_8859_1))) {
377                 for (String value : lines) {
378                     writer.write(value);
379                     writer.newLine();
380                 }
381 
382                 // write remaining lines
383                 Enumeration<?> propertyEnumeration = properties.propertyNames();
384                 while (propertyEnumeration.hasMoreElements()) {
385                     String propertyName = (String) propertyEnumeration.nextElement();
386                     writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
387                     writer.newLine();
388                 }
389             } catch (IOException e) {
390                 DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
391             }
392         }
393         updateLoggingConfig();
394     }
395 
396     private static void readLines(ArrayList<String> lines, Properties properties) {
397         try {
398             File configFile = new File(configFilePath);
399             if (configFile.exists()) {
400                 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(configFile.toPath()), StandardCharsets.ISO_8859_1))) {
401                     String line;
402                     while ((line = reader.readLine()) != null) {
403                         lines.add(convertLine(line, properties));
404                     }
405                 }
406             }
407         } catch (IOException e) {
408             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
409         }
410     }
411 
412     /**
413      * Convert input property line to new line with value from properties.
414      * Preserve comments
415      *
416      * @param line       input line
417      * @param properties new property values
418      * @return new line
419      */
420     private static String convertLine(String line, Properties properties) {
421         int hashIndex = line.indexOf('#');
422         int equalsIndex = line.indexOf('=');
423         // allow # in values, no a comment
424         // comments are pass through
425         if (equalsIndex >= 0 && (hashIndex < 0 || hashIndex >= equalsIndex)) {
426             String key = line.substring(0, equalsIndex);
427             String value = properties.getProperty(key);
428             if (value != null) {
429                 // build property with new value
430                 line = key + "=" + escapeValue(value);
431                 // remove property from source
432                 properties.remove(key);
433             }
434         }
435         return line;
436     }
437 
438     /**
439      * Escape backslash in value.
440      *
441      * @param value value
442      * @return escaped value
443      */
444     private static String escapeValue(String value) {
445         StringBuilder buffer = new StringBuilder();
446         for (char c : value.toCharArray()) {
447             if (c == '\\') {
448                 buffer.append('\\');
449             }
450             buffer.append(c);
451         }
452         return buffer.toString();
453     }
454 
455 
456     /**
457      * Get a property value as String.
458      *
459      * @param property property name
460      * @return property value
461      */
462     public static synchronized String getProperty(String property) {
463         String value = SETTINGS_PROPERTIES.getProperty(property);
464         // return null on empty value
465         if (value != null && value.isEmpty()) {
466             value = null;
467         }
468         return value;
469     }
470 
471     /**
472      * Get property value or default.
473      *
474      * @param property     property name
475      * @param defaultValue default property value
476      * @return property value
477      */
478     public static synchronized String getProperty(String property, String defaultValue) {
479         String value = getProperty(property);
480         if (value == null) {
481             value = defaultValue;
482         }
483         return value;
484     }
485 
486 
487     /**
488      * Get a property value as char[].
489      *
490      * @param property property name
491      * @return property value
492      */
493     public static synchronized char[] getCharArrayProperty(String property) {
494         String propertyValue = Settings.getProperty(property);
495         char[] value = null;
496         if (propertyValue != null) {
497             value = propertyValue.toCharArray();
498         }
499         return value;
500     }
501 
502     /**
503      * Set a property value.
504      *
505      * @param property property name
506      * @param value    property value
507      */
508     public static synchronized void setProperty(String property, String value) {
509         if (value != null) {
510             SETTINGS_PROPERTIES.setProperty(property, value);
511         } else {
512             SETTINGS_PROPERTIES.setProperty(property, "");
513         }
514     }
515 
516     /**
517      * Get a property value as int.
518      *
519      * @param property property name
520      * @return property value
521      */
522     public static synchronized int getIntProperty(String property) {
523         return getIntProperty(property, 0);
524     }
525 
526     /**
527      * Get a property value as int, return default value if null.
528      *
529      * @param property     property name
530      * @param defaultValue default property value
531      * @return property value
532      */
533     public static synchronized int getIntProperty(String property, int defaultValue) {
534         int value = defaultValue;
535         try {
536             String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
537             if (propertyValue != null && !propertyValue.isEmpty()) {
538                 value = Integer.parseInt(propertyValue);
539             }
540         } catch (NumberFormatException e) {
541             DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
542         }
543         return value;
544     }
545 
546     /**
547      * Get a property value as boolean.
548      *
549      * @param property property name
550      * @return property value
551      */
552     public static synchronized boolean getBooleanProperty(String property) {
553         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
554         return Boolean.parseBoolean(propertyValue);
555     }
556 
557     /**
558      * Get a property value as boolean.
559      *
560      * @param property     property name
561      * @param defaultValue default property value
562      * @return property value
563      */
564     public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
565         boolean value = defaultValue;
566         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
567         if (propertyValue != null && !propertyValue.isEmpty()) {
568             value = Boolean.parseBoolean(propertyValue);
569         }
570         return value;
571     }
572 
573     public static synchronized String loadRefreshToken(String username) {
574         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
575         if (isEmpty(tokenFilePath)) {
576             return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
577         } else {
578             return loadtokenFromFile(tokenFilePath, username.toLowerCase());
579         }
580     }
581 
582 
583     public static synchronized void storeRefreshToken(String username, String refreshToken) {
584         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
585         if (isEmpty(tokenFilePath)) {
586             Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
587             Settings.save();
588         } else {
589             savetokentoFile(tokenFilePath, username.toLowerCase(), refreshToken);
590         }
591     }
592 
593     /**
594      * Persist token in davmail.oauth.tokenFilePath.
595      *
596      * @param tokenFilePath token file path
597      * @param username      username
598      * @param refreshToken  Oauth refresh token
599      */
600     private static void savetokentoFile(String tokenFilePath, String username, String refreshToken) {
601         try {
602             checkCreateTokenFilePath(tokenFilePath);
603             Properties properties = new Properties();
604             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
605                 properties.load(fis);
606             }
607             properties.setProperty(username, refreshToken);
608             try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
609                 properties.store(fos, "Oauth tokens");
610             }
611         } catch (IOException e) {
612             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
613         }
614     }
615 
616     /**
617      * Load token from davmail.oauth.tokenFilePath.
618      *
619      * @param tokenFilePath token file path
620      * @param username      username
621      * @return encrypted token value
622      */
623     private static String loadtokenFromFile(String tokenFilePath, String username) {
624         try {
625             checkCreateTokenFilePath(tokenFilePath);
626             Properties properties = new Properties();
627             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
628                 properties.load(fis);
629             }
630             return properties.getProperty(username);
631         } catch (IOException e) {
632             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
633         }
634         return null;
635     }
636 
637     private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
638         File file = new File(tokenFilePath);
639         File parentFile = file.getParentFile();
640         if (parentFile != null && (parentFile.mkdirs())) {
641             LOGGER.info("Created token file directory " + parentFile.getAbsolutePath());
642 
643         }
644         if (file.createNewFile()) {
645             LOGGER.info("Created token file " + tokenFilePath);
646         }
647     }
648 
649     /**
650      * Build logging properties prefix.
651      *
652      * @param category logging category
653      * @return prefix
654      */
655     private static String getLoggingPrefix(String category) {
656         String prefix;
657         if ("rootLogger".equals(category)) {
658             prefix = "log4j.";
659         } else {
660             prefix = "log4j.logger.";
661         }
662         return prefix;
663     }
664 
665     /**
666      * Return Log4J logging level for the category.
667      *
668      * @param category logging category
669      * @return logging level
670      */
671     public static synchronized Level getLoggingLevel(String category) {
672         String prefix = getLoggingPrefix(category);
673         String currentValue = SETTINGS_PROPERTIES.getProperty(prefix + category);
674 
675         if (currentValue != null && !currentValue.isEmpty()) {
676             return Level.toLevel(currentValue);
677         } else if ("rootLogger".equals(category)) {
678             return Logger.getRootLogger().getLevel();
679         } else {
680             return Logger.getLogger(category).getLevel();
681         }
682     }
683 
684     /**
685      * Get all properties that are in the specified scope, that is, that start with '&lt;scope&gt;.'.
686      *
687      * @param scope start of property name
688      * @return properties
689      */
690     public static synchronized Properties getSubProperties(String scope) {
691         final String keyStart;
692         if (scope == null || scope.isEmpty()) {
693             keyStart = "";
694         } else if (scope.endsWith(".")) {
695             keyStart = scope;
696         } else {
697             keyStart = scope + '.';
698         }
699         Properties result = new Properties();
700         for (Map.Entry<Object, Object> entry : SETTINGS_PROPERTIES.entrySet()) {
701             String key = (String) entry.getKey();
702             if (key.startsWith(keyStart)) {
703                 String value = (String) entry.getValue();
704                 result.setProperty(key.substring(keyStart.length()), value);
705             }
706         }
707         return result;
708     }
709 
710     /**
711      * Set Log4J logging level for the category
712      *
713      * @param category logging category
714      * @param level    logging level
715      */
716     public static synchronized void setLoggingLevel(String category, Level level) {
717         if (level != null) {
718             String prefix = getLoggingPrefix(category);
719             SETTINGS_PROPERTIES.setProperty(prefix + category, level.toString());
720             if ("rootLogger".equals(category)) {
721                 Logger.getRootLogger().setLevel(level);
722             } else {
723                 Logger.getLogger(category).setLevel(level);
724             }
725         }
726     }
727 
728     /**
729      * Change and save a single property.
730      *
731      * @param property property name
732      * @param value    property value
733      */
734     public static synchronized void saveProperty(String property, String value) {
735         Settings.load();
736         Settings.setProperty(property, value);
737         Settings.save();
738     }
739 
740     /**
741      * Test if running on Windows
742      *
743      * @return true on Windows
744      */
745     public static boolean isWindows() {
746         return System.getProperty("os.name").toLowerCase().startsWith("windows");
747     }
748 
749     /**
750      * Test if running on Linux
751      *
752      * @return true on Linux
753      */
754     public static boolean isLinux() {
755         return System.getProperty("os.name").toLowerCase().startsWith("linux");
756     }
757 
758     public static boolean isUnix() {
759         return isLinux() ||
760                 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
761     }
762 
763     public static String getUserAgent() {
764         return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
765     }
766 
767     public static String getOutlookUrl() {
768         String tld = getProperty("davmail.tld");
769         String outlookUrl = getProperty("davmail.outlookUrl");
770         if (outlookUrl != null) {
771             return outlookUrl;
772         } else if (tld == null) {
773             return OUTLOOK_URL;
774         } else {
775             return "https://outlook.office365." + tld;
776         }
777     }
778 
779     public static String getO365Url() {
780         String tld = getProperty("davmail.tld");
781         String outlookUrl = getProperty("davmail.outlookUrl");
782         if (outlookUrl != null) {
783             return outlookUrl + "/EWS/Exchange.asmx";
784         } else if (tld == null) {
785             return O365_URL;
786         } else {
787             return "https://outlook.office365." + tld + "/EWS/Exchange.asmx";
788         }
789     }
790 
791     /**
792      * Handle custom graph endpoints.
793      * See <a href="https://learn.microsoft.com/en-us/graph/deployments">...</a>
794      * @return graph endpoint url
795      */
796     public static String getGraphUrl() {
797         String tld = getProperty("davmail.tld");
798         String graphUrl = getProperty("davmail.graphUrl");
799         if (graphUrl != null) {
800             return graphUrl;
801         } else if (tld == null) {
802             return GRAPH_URL;
803         } else {
804             return "https://graph.microsoft." + tld;
805         }
806     }
807 
808     public static String getO365LoginUrl() {
809         String tld = getProperty("davmail.tld");
810         String loginUrl = getProperty("davmail.loginUrl");
811         if (loginUrl != null) {
812             return loginUrl;
813         } else if (tld == null) {
814             return O365_LOGIN_URL;
815         } else {
816             return "https://login.microsoftonline." + tld;
817         }
818     }
819 
820     public static boolean isSWTAvailable() {
821         boolean isSWTAvailable = false;
822         ClassLoader classloader = Settings.class.getClassLoader();
823         try {
824             // trigger ClassNotFoundException
825             classloader.loadClass("org.eclipse.swt.SWT");
826             isSWTAvailable = true;
827         } catch (Throwable e) {
828             LOGGER.info(new BundleMessage("LOG_SWT_NOT_AVAILABLE"));
829         }
830         return isSWTAvailable;
831     }
832 
833     public static boolean isJFXAvailable() {
834         boolean isJFXAvailable = false;
835         try {
836             Class.forName("javafx.application.Platform");
837             isJFXAvailable = true;
838         } catch (ClassNotFoundException | NullPointerException e) {
839             LOGGER.info("JavaFX (OpenJFX) not available");
840         }
841         return isJFXAvailable;
842     }
843 
844     public static boolean isDocker() {
845         boolean isDocker = new File("/.dockerenv").exists();
846         if (isDocker) {
847             LOGGER.info("Running in docker");
848         }
849         return isDocker;
850     }
851 
852 }