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