diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 29ff20015..3bd731520 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ - - - @@ -75,6 +72,12 @@ + + + + + + diff --git a/assets/emoticons/24hours.gif b/assets/emoticons/24hours.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/airplane.gif b/assets/emoticons/airplane.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/angry.gif b/assets/emoticons/angry.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/annoy.gif b/assets/emoticons/annoy.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/apple.gif b/assets/emoticons/apple.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/appli01.gif b/assets/emoticons/appli01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/appli02.gif b/assets/emoticons/appli02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/aquarius.gif b/assets/emoticons/aquarius.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/aries.gif b/assets/emoticons/aries.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/art.gif b/assets/emoticons/art.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/atm.gif b/assets/emoticons/atm.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bag.gif b/assets/emoticons/bag.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ban.gif b/assets/emoticons/ban.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/banana.gif b/assets/emoticons/banana.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bank.gif b/assets/emoticons/bank.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bar.gif b/assets/emoticons/bar.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/baseball.gif b/assets/emoticons/baseball.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/basketball.gif b/assets/emoticons/basketball.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bearing.gif b/assets/emoticons/bearing.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/beer.gif b/assets/emoticons/beer.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bell.gif b/assets/emoticons/bell.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bicycle.gif b/assets/emoticons/bicycle.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/birthday.gif b/assets/emoticons/birthday.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bleah.gif b/assets/emoticons/bleah.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bomb.gif b/assets/emoticons/bomb.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/book.gif b/assets/emoticons/book.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bottle.gif b/assets/emoticons/bottle.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/boutique.gif b/assets/emoticons/boutique.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bread.gif b/assets/emoticons/bread.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bud.gif b/assets/emoticons/bud.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/building.gif b/assets/emoticons/building.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bullettrain.gif b/assets/emoticons/bullettrain.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/bus.gif b/assets/emoticons/bus.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/by-d.gif b/assets/emoticons/by-d.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cafe.gif b/assets/emoticons/cafe.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cake.gif b/assets/emoticons/cake.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/camera.gif b/assets/emoticons/camera.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cancer.gif b/assets/emoticons/cancer.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/capricornus.gif b/assets/emoticons/capricornus.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/car.gif b/assets/emoticons/car.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/carouselpony.gif b/assets/emoticons/carouselpony.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cat.gif b/assets/emoticons/cat.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/catface.gif b/assets/emoticons/catface.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cd.gif b/assets/emoticons/cd.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/chair.gif b/assets/emoticons/chair.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cherry.gif b/assets/emoticons/cherry.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cherryblossom.gif b/assets/emoticons/cherryblossom.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/chick.gif b/assets/emoticons/chick.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/clear.gif b/assets/emoticons/clear.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/clip.gif b/assets/emoticons/clip.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/clock.gif b/assets/emoticons/clock.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cloud.gif b/assets/emoticons/cloud.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/clover.gif b/assets/emoticons/clover.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/club.gif b/assets/emoticons/club.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/coldsweats01.gif b/assets/emoticons/coldsweats01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/coldsweats02.gif b/assets/emoticons/coldsweats02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/confident.gif b/assets/emoticons/confident.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/copyright.gif b/assets/emoticons/copyright.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/crown.gif b/assets/emoticons/crown.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/crying.gif b/assets/emoticons/crying.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/cute.gif b/assets/emoticons/cute.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/d-point.gif b/assets/emoticons/d-point.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/danger.gif b/assets/emoticons/danger.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/dash.gif b/assets/emoticons/dash.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/delicious.gif b/assets/emoticons/delicious.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/denim.gif b/assets/emoticons/denim.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/despair.gif b/assets/emoticons/despair.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/diamond.gif b/assets/emoticons/diamond.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/dog.gif b/assets/emoticons/dog.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/dollar.gif b/assets/emoticons/dollar.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/door.gif b/assets/emoticons/door.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/down.gif b/assets/emoticons/down.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/downwardleft.gif b/assets/emoticons/downwardleft.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/downwardright.gif b/assets/emoticons/downwardright.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/drama.gif b/assets/emoticons/drama.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ear.gif b/assets/emoticons/ear.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/eight.gif b/assets/emoticons/eight.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/empty.gif b/assets/emoticons/empty.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/end.gif b/assets/emoticons/end.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/enter.gif b/assets/emoticons/enter.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/event.gif b/assets/emoticons/event.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/eye.gif b/assets/emoticons/eye.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/eyeglass.gif b/assets/emoticons/eyeglass.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/fastfood.gif b/assets/emoticons/fastfood.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/faxto.gif b/assets/emoticons/faxto.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/fish.gif b/assets/emoticons/fish.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/five.gif b/assets/emoticons/five.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/flag.gif b/assets/emoticons/flag.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/flair.gif b/assets/emoticons/flair.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/foot.gif b/assets/emoticons/foot.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/four.gif b/assets/emoticons/four.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/free.gif b/assets/emoticons/free.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/freedial.gif b/assets/emoticons/freedial.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/fuji.gif b/assets/emoticons/fuji.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/full.gif b/assets/emoticons/full.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/fullmoon.gif b/assets/emoticons/fullmoon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/game.gif b/assets/emoticons/game.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/gasstation.gif b/assets/emoticons/gasstation.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/gawk.gif b/assets/emoticons/gawk.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/gemini.gif b/assets/emoticons/gemini.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/golf.gif b/assets/emoticons/golf.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/good.gif b/assets/emoticons/good.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/hairsalon.gif b/assets/emoticons/hairsalon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/happy01.gif b/assets/emoticons/happy01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/happy02.gif b/assets/emoticons/happy02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/heart.gif b/assets/emoticons/heart.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/heart01.gif b/assets/emoticons/heart01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/heart02.gif b/assets/emoticons/heart02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/heart03.gif b/assets/emoticons/heart03.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/heart04.gif b/assets/emoticons/heart04.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/horse.gif b/assets/emoticons/horse.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/hospital.gif b/assets/emoticons/hospital.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/hotel.gif b/assets/emoticons/hotel.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/house.gif b/assets/emoticons/house.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/id.gif b/assets/emoticons/id.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/impact.gif b/assets/emoticons/impact.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/info01.gif b/assets/emoticons/info01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/info02.gif b/assets/emoticons/info02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/japanesetea.gif b/assets/emoticons/japanesetea.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/karaoke.gif b/assets/emoticons/karaoke.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/key.gif b/assets/emoticons/key.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/kissmark.gif b/assets/emoticons/kissmark.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/leftright.gif b/assets/emoticons/leftright.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/leo.gif b/assets/emoticons/leo.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/libra.gif b/assets/emoticons/libra.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/loveletter.gif b/assets/emoticons/loveletter.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/lovely.gif b/assets/emoticons/lovely.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/mail.gif b/assets/emoticons/mail.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/mailto.gif b/assets/emoticons/mailto.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/maple.gif b/assets/emoticons/maple.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/memo.gif b/assets/emoticons/memo.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/mist.gif b/assets/emoticons/mist.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/mobaq.gif b/assets/emoticons/mobaq.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/mobilephone.gif b/assets/emoticons/mobilephone.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/moneybag.gif b/assets/emoticons/moneybag.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/moon1.gif b/assets/emoticons/moon1.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/moon2.gif b/assets/emoticons/moon2.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/moon3.gif b/assets/emoticons/moon3.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/motorsports.gif b/assets/emoticons/motorsports.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/movie.gif b/assets/emoticons/movie.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/music.gif b/assets/emoticons/music.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/new.gif b/assets/emoticons/new.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/newmoon.gif b/assets/emoticons/newmoon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ng.gif b/assets/emoticons/ng.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/night.gif b/assets/emoticons/night.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/nine.gif b/assets/emoticons/nine.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/noodle.gif b/assets/emoticons/noodle.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/nosmoking.gif b/assets/emoticons/nosmoking.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/note.gif b/assets/emoticons/note.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/notes.gif b/assets/emoticons/notes.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ok.gif b/assets/emoticons/ok.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/on.gif b/assets/emoticons/on.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/one.gif b/assets/emoticons/one.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/paper.gif b/assets/emoticons/paper.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/parking.gif b/assets/emoticons/parking.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pass.gif b/assets/emoticons/pass.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pc.gif b/assets/emoticons/pc.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pen.gif b/assets/emoticons/pen.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pencil.gif b/assets/emoticons/pencil.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/penguin.gif b/assets/emoticons/penguin.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/phoneto.gif b/assets/emoticons/phoneto.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pig.gif b/assets/emoticons/pig.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pisces.gif b/assets/emoticons/pisces.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pocketbell.gif b/assets/emoticons/pocketbell.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/postoffice.gif b/assets/emoticons/postoffice.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pouch.gif b/assets/emoticons/pouch.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/pout.gif b/assets/emoticons/pout.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/present.gif b/assets/emoticons/present.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/punch.gif b/assets/emoticons/punch.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/r-mark.gif b/assets/emoticons/r-mark.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/rain.gif b/assets/emoticons/rain.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/recycle.gif b/assets/emoticons/recycle.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/restaurant.gif b/assets/emoticons/restaurant.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ribbon.gif b/assets/emoticons/ribbon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/riceball.gif b/assets/emoticons/riceball.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ring.gif b/assets/emoticons/ring.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/rock.gif b/assets/emoticons/rock.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/rouge.gif b/assets/emoticons/rouge.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/run.gif b/assets/emoticons/run.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/rvcar.gif b/assets/emoticons/rvcar.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sad.gif b/assets/emoticons/sad.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sagittarius.gif b/assets/emoticons/sagittarius.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sandclock.gif b/assets/emoticons/sandclock.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/school.gif b/assets/emoticons/school.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/scissors.gif b/assets/emoticons/scissors.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/scorpius.gif b/assets/emoticons/scorpius.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/search.gif b/assets/emoticons/search.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/secret.gif b/assets/emoticons/secret.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/seven.gif b/assets/emoticons/seven.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/shadow.gif b/assets/emoticons/shadow.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sharp.gif b/assets/emoticons/sharp.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/shine.gif b/assets/emoticons/shine.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ship.gif b/assets/emoticons/ship.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/shock.gif b/assets/emoticons/shock.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/shoe.gif b/assets/emoticons/shoe.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sign01.gif b/assets/emoticons/sign01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sign02.gif b/assets/emoticons/sign02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sign03.gif b/assets/emoticons/sign03.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sign04.gif b/assets/emoticons/sign04.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sign05.gif b/assets/emoticons/sign05.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/signaler.gif b/assets/emoticons/signaler.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/six.gif b/assets/emoticons/six.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ski.gif b/assets/emoticons/ski.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/slate.gif b/assets/emoticons/slate.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sleepy.gif b/assets/emoticons/sleepy.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/smile.gif b/assets/emoticons/smile.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/smoking.gif b/assets/emoticons/smoking.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/snail.gif b/assets/emoticons/snail.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/snow.gif b/assets/emoticons/snow.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/snowboard.gif b/assets/emoticons/snowboard.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/soccer.gif b/assets/emoticons/soccer.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/soon.gif b/assets/emoticons/soon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/spa.gif b/assets/emoticons/spa.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/spade.gif b/assets/emoticons/spade.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sports.gif b/assets/emoticons/sports.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sprinkle.gif b/assets/emoticons/sprinkle.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/subway.gif b/assets/emoticons/subway.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sun.gif b/assets/emoticons/sun.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sweat01.gif b/assets/emoticons/sweat01.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/sweat02.gif b/assets/emoticons/sweat02.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/t-shirt.gif b/assets/emoticons/t-shirt.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/taurus.gif b/assets/emoticons/taurus.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/telephone.gif b/assets/emoticons/telephone.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/tennis.gif b/assets/emoticons/tennis.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/think.gif b/assets/emoticons/think.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/three.gif b/assets/emoticons/three.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/thunder.gif b/assets/emoticons/thunder.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/ticket.gif b/assets/emoticons/ticket.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/tm.gif b/assets/emoticons/tm.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/toilet.gif b/assets/emoticons/toilet.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/train.gif b/assets/emoticons/train.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/tulip.gif b/assets/emoticons/tulip.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/tv.gif b/assets/emoticons/tv.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/two.gif b/assets/emoticons/two.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/typhoon.gif b/assets/emoticons/typhoon.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/up.gif b/assets/emoticons/up.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/updown.gif b/assets/emoticons/updown.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/upwardleft.gif b/assets/emoticons/upwardleft.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/upwardright.gif b/assets/emoticons/upwardright.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/virgo.gif b/assets/emoticons/virgo.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/watch.gif b/assets/emoticons/watch.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wave.gif b/assets/emoticons/wave.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/weep.gif b/assets/emoticons/weep.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wheelchair.gif b/assets/emoticons/wheelchair.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wine.gif b/assets/emoticons/wine.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wink.gif b/assets/emoticons/wink.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wobbly.gif b/assets/emoticons/wobbly.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/wrench.gif b/assets/emoticons/wrench.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/xmas.gif b/assets/emoticons/xmas.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/yacht.gif b/assets/emoticons/yacht.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/yen.gif b/assets/emoticons/yen.gif old mode 100755 new mode 100644 diff --git a/assets/emoticons/zero.gif b/assets/emoticons/zero.gif old mode 100755 new mode 100644 diff --git a/res/drawable-hdpi/ic_button_mark_read.png b/res/drawable-hdpi/ic_button_mark_read.png old mode 100755 new mode 100644 diff --git a/res/drawable-hdpi/ic_button_mark_unread.png b/res/drawable-hdpi/ic_button_mark_unread.png old mode 100755 new mode 100644 diff --git a/res/drawable-hdpi/ic_menu_identity.png b/res/drawable-hdpi/ic_menu_identity.png old mode 100755 new mode 100644 diff --git a/res/drawable/button_indicator_prev.png b/res/drawable/button_indicator_prev.png old mode 100755 new mode 100644 diff --git a/res/drawable/colorpicker_menjadi.png b/res/drawable/colorpicker_menjadi.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_button_mark_read.png b/res/drawable/ic_button_mark_read.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_button_mark_unread.png b/res/drawable/ic_button_mark_unread.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_menu_identity.png b/res/drawable/ic_menu_identity.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_menu_navigate.png b/res/drawable/ic_menu_navigate.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_menu_reverse_sort.png b/res/drawable/ic_menu_reverse_sort.png old mode 100755 new mode 100644 diff --git a/res/drawable/ic_menu_set_sort.png b/res/drawable/ic_menu_set_sort.png old mode 100755 new mode 100644 diff --git a/res/drawable/icon.png b/res/drawable/icon.png old mode 100755 new mode 100644 diff --git a/res/layout/accounts_password_prompt.xml b/res/layout/accounts_password_prompt.xml new file mode 100644 index 000000000..55ae15037 --- /dev/null +++ b/res/layout/accounts_password_prompt.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/res/menu/accounts_context.xml b/res/menu/accounts_context.xml index 798d32d20..9eb952915 100644 --- a/res/menu/accounts_context.xml +++ b/res/menu/accounts_context.xml @@ -21,6 +21,8 @@ android:title="@string/remove_account_action" /> + + + + + + + diff --git a/res/menu/disabled_accounts_context.xml b/res/menu/disabled_accounts_context.xml new file mode 100644 index 000000000..54d6c2afc --- /dev/null +++ b/res/menu/disabled_accounts_context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 7b25cc3ed..8c932dd6f 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -1050,6 +1050,47 @@ Benvingut a la configuració del K-9. El K-9 és un client de codi obert per An No es pot connectar. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Compte \"%s\" no és disponible; comprova emmagatzematge Desa adjunts a... @@ -1058,5 +1099,6 @@ Benvingut a la configuració del K-9. El K-9 és un client de codi obert per An + diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 94781ad96..e7dd913fe 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -1056,6 +1056,47 @@ Vítejte v nastavení pošty K-9 Mail. K-9 je open source poštovní klient pro Nelze se připojit. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1064,5 +1105,6 @@ Vítejte v nastavení pošty K-9 Mail. K-9 je open source poštovní klient pro + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 942ccea78..5cc76a5ea 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -1036,6 +1036,47 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü Verbindungsfehler. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Konto \"%s\" ist nicht verfügbar; Bitte SD-Karte prüfen. Anhang speichern unter... @@ -1044,5 +1085,6 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü Nach oben verschieben Nach unten verschieben + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 995b7aca1..7df346258 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -1046,6 +1046,47 @@ Bienvenido a la Configuración de K-9. K-9 es un cliente de correo OpenSource pa Imposible conectar. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1054,5 +1095,6 @@ Bienvenido a la Configuración de K-9. K-9 es un cliente de correo OpenSource pa + diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 4496c4658..ee0273138 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -1043,6 +1043,47 @@ Tervetuloa K-9 Mail asennukseen.  K-9 on avoimen lähdekoodin sähköpostiasiak Yhteyden muodostus epäonnistui. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1051,5 +1092,6 @@ Tervetuloa K-9 Mail asennukseen.  K-9 on avoimen lähdekoodin sähköpostiasiak + diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 5c02c872d..91742ab09 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -1,319 +1,363 @@ - - K-9 Mail - K-9 Mail BETA - Google, Les K-9 Dog Walkers. - Auteurs\u00A0: %s - - Information de révision\u00A0: %s +Editor: +ArtfulBits Inc. (c) 2005-2009 (http://www.artfulbits.com) +Android Localizer (aiLocalizer) +--> + + K-9 Mail + K-9 Mail BETA + Google, Les K-9 Dog Walkers. + Copyright 2008-%s The K-9 Dog Walkers. Portions Copyright 2006-%s the Android Open Source Project. + Licensed under the Apache License, Version 2.0. + Auteurs\u00A0: %s + Information de révision\u00A0: %s Tierces librairies utilisées\u00A0: %s Icônes Emoji\u00A0: %s - lire les pièces jointes - Permet à cette application de lire vos pièces jointes. + lire les pièces jointes + Permet à cette application de lire vos pièces jointes. + lire les emails + Permet à cette application de lire vos emails. + Supprimer les emails + Permet à cette application d\'effacer vos emails. - À propos de %s - Comptes - Avancé - %s - Comptes K-9 + À propos de %s + Comptes + Avancé + %s + Comptes K-9 - %s:%s + %s:%s - Composer - Déboguer - Choisir un dossier - Choisir une couleur + Composer + Déboguer + Choisir un dossier + Choisir une couleur - %s%s%s + %s%s%s - \u0020[%d] - \u0020(Récup. %s:%s%s) - \u0020(Récup. entêtes %s:%s%s) - \u0020(Envoi %s%s) - \u0020(Prép %s:%s%s) - \u0020%s/%s + \u0020[%d] + \u0020(Récup. %s:%s%s) + \u0020(Récup. entêtes %s:%s%s) + \u0020(Envoi %s%s) + \u0020(Prép %s:%s%s) + \u0020%s/%s - \u0020(Prochaine récupération à %s) + \u0020(Prochaine récupération à %s) \u0020(synchronisation désactivée) - Suivant - OK - Annuler - Envoyer - Envoyer à nouveau - Sélectionner - Désélectionner - Répondre - Répondre à tous - Supprimer - Archiver - Spam - Vider le dossier - Transférer - Déplacer - Continuer - Terminé - Supprimer - Abandonner - Enregistrer dans les brouillons - Réessayer - Actualiser - Vérif. messages - Envoyer les messages - Liste des dossiers - Rafraîchir les dossiers - Marquer tous les messages comme lus - Ajouter un compte - Composer - Rechercher - Résultats de la recherche - Préférences - Ouvrir - Paramètres du compte - Paramètres du dossier - Paramètres globaux - Supprimer le compte - Annuler les actions en attente (Danger\u00A0!) - Comptes - - - Lire - Marquer comme lu - Transférer avec une autre application - Choisir l\'expéditeur - Marquer tous les messages comme lus - Marquer tous les messages dans \'%s\' comme lus\u00A0? (incluant les messages dans le dossier qui ne sont pas affichés\u00A0?) - Ajouter une étoile - Enlever l\'étoile - Copier - Montrer l\'entête complète - Masquer l\'entête complète - Sélection de texte - Marquer comme non lu - Déplacer vers - Dossiers - Afficher/masquer les détails - Ajouter Cc/Cci - Modifier l\'objet - Ajouter une pièce jointe - Joindre une image - Joindre une vidéo - Paramètres de décharge - Vider la corbeille - Éliminer les messages supprimés + + Suivant + Précédent + OK + Annuler + Envoyer + Envoyer à nouveau + Sélectionner + Désélectionner + Répondre + Répondre à tous + Supprimer + Archiver + Spam + Vider le dossier + Transférer + Déplacer + Continuer + Terminé + Supprimer + Abandonner + Enregistrer dans les brouillons + Réessayer + Actualiser + Vérif. messages + Envoyer les messages + Liste des dossiers + Rafraîchir les dossiers + Marquer tous les messages comme lus + Ajouter un compte + Composer + Rechercher + Résultats de la recherche + Préférences + Ouvrir + Paramètres du compte + Paramètres du dossier + Paramètres globaux + Supprimer le compte + Annuler les actions en attente (Danger\u00A0!) + + Comptes + + + Lire + Marquer comme lu + Transférer avec une autre application + Choisir l\'expéditeur + + Marquer tous les messages comme lus + Marquer tous les messages dans \'%s\' comme lus\u00A0? (incluant + les messages dans le dossier qui ne sont pas affichés\u00A0?) + + Ajouter une étoile + Enlever l\'étoile + Copier + Montrer l\'entête complète + Masquer l\'entête complète + Sélection de texte + + Marquer comme non lu + Déplacer vers + Dossiers + Afficher/masquer les détails + Ajouter Cc/Cci + Modifier l\'objet + Accusé de réception + Un accusé de réception sera demandé + Un accusé de réception ne sera pas demandé + Ajouter une pièce jointe + Joindre une image + Joindre une vidéo + Paramètres de décharge + Vider la corbeille + Éliminer les messages supprimés Effacer les messages locaux - Choisir le tri - Inverser le tri - À propos - Préférences - Options du compte - Options du dossier - (Aucun objet) + Choisir le tri + Inverser le tri + À propos + + Préférences + Options du compte + Options du dossier + + (Aucun objet) Pas de date - Aucun expéditeur - Récupération\u2026 - (Récupération %s%s) - Chargement des messages\u2026 - Erreur de connexion - Message non trouvé - Erreur - Envoi\u2026 - Réessayer de charger plus de messages - Charger jusqu\'à %d de plus - Go - Mo - ko - oct. - + Aucun expéditeur + Récupération\u2026 + (Récupération %s%s) + Chargement des messages\u2026 + Erreur de connexion + Message non trouvé + Erreur + Envoi\u2026 + + Réessayer de charger plus de messages + + Charger jusqu\'à %d +de plus + + Go + Mo + ko + oct. + + Espace sur «\u00A0%s\u00A0» passé de %s à %s - - Compactage du compte «\u00A0%s\u00A0» - Effacement du compte «\u00A0%s\u00A0» - Recréation du compte «\u00A0%s\u00A0» - Nouvel e-mail - Nouvel e-mail de %s - %d non lu(s) (%s) - %d nouveaux messages (%s) - dans les comptes %d - Message non envoyé + - Vérification des messages\u00A0: %s:%s - Vérification des messages - Envoi du message\u00A0: %s - Envoi du message - : - Boîte de réception - Boîte d\'envoi - Brouillons - Corbeille - Envoyés - Archive - Spams + Compactage du compte «\u00A0%s\u00A0» + Effacement du compte «\u00A0%s\u00A0» + Recréation du compte «\u00A0%s\u00A0» - %s (Brouillons) - %s (Corbeille) - %s (Envoyés) - %s (Archive) - %s (Spams) + Nouvel e-mail + Nouvel e-mail de %s + %d non lu(s) (%s) + %d nouveaux messages (%s) + dans les comptes %d + Message non envoyé - Échec d\'envoi de certains messages - Voir le dossier %s pour les détails. - K-9 a rencontré un problème lors de l\'envoi de certains messages. - Cependant, dû à la nature de ce problème, K-9 ne peut être certain si les messages + Vérification des messages\u00A0: %s:%s + Vérification des messages + Envoi du message\u00A0: %s + Envoi du message + : + + Boîte de réception + Boîte d\'envoi + + Brouillons + Corbeille + Envoyés + Archive + Spams + + + %s (Brouillons) + %s (Corbeille) + %s (Envoyés) + %s (Archive) + %s (Spams) + + Échec d\'envoi de certains messages + Voir le dossier %s pour les détails. + K-9 a rencontré un problème lors de l\'envoi de certains messages. + Cependant, dû à la nature de ce problème, K-9 ne peut être certain si les messages ont été envoyés. Les destinataires ont peut-être déjà reçu une copie des messages. \u000a\u000aLes messages qui sont touchés par ce problème sont maintenant marqués d\'une étoile dans votre boîte d\'envoi. Si vous enlevez ces étoiles, K-9 essaiera de les envoyer de nouveau. Maintenez votre doigt sur la boîte d\'envoi et sélectionnez «\u00A0Envoyer les messages\u00A0» pour initier l\'envoi.\u000A\u000a Le dossier %s pourrait contenir des messages d\'erreur à propos de ces échecs. - Alerte K-9 - Synchronisation et envoi suspendus dû au réseau inaccessible. - Aucun message - Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messagerie open source pour Android basé sur le client standard Android Mail. -\n -\n\nVoici ce que K-9 Mail apporte de plus\u00A0: -\n * La technologie PUSH en passant par le protocole IMAP -\n * De meilleures performances -\n * Signer ses messages -\n * S\'envoyer une copie des messages automatiquement -\n * Choisir les dossiers à synchroniser -\n * Choisir l\'adresse e-mail de réponse -\n * Des raccourcis clavier -\n * Un meilleur support de l\'IMAP -\n * Sauver les pièces jointes sur la carte SD -\n * Vider la corbeille -\n * Trier les messages -\n * ...et bien plus encore -\n -\n Veuillez noter que K-9 Mail ne prend pas en charge les comptes Hotmail et, comme d\'autres clients, a quelques ennuis avec les serveurs Microsoft Exchange. -\n -\n Si vous avez des suggestions, découvert des bugs ou simplement une idée de fonctionnalité, n\'hésitez pas à vous rendre ici\u00A0: http://k9mail.googlecode.com/ (en anglais) - - Version\u00A0: %s - Activer le journal de déboguage - Journaliser les informations de diagnostic supplémentaires - Journaliser les informations personnelles - Vos mots de passe pourront se retrouver dans le journal - K-9 Mail pour Android - Tous les messages - Messages récents de tous les comptes - Toutes les boîtes de réception + Alerte K-9 + Synchronisation et envoi suspendus dû au réseau inaccessible. + + Aucun message + + +Bienvenue dans la configuration de K-9 Mail. K-9 Mail est un client de messagerie open source pour Android basé sur le client standard Android Mail. + \n + \n\nVoici ce que K-9 Mail apporte de plus\u00A0: + \n * La technologie PUSH en passant par le protocole IMAP + \n * De meilleures performances + \n * Signer ses messages + \n * S\'envoyer une copie des messages automatiquement + \n * Choisir les dossiers à synchroniser + \n * Choisir l\'adresse e-mail de réponse + \n * Des raccourcis clavier + \n * Un meilleur support de l\'IMAP + \n * Sauver les pièces jointes sur la carte SD + \n * Vider la corbeille + \n * Trier les messages + \n * ...et bien plus encore + \n + \n Veuillez noter que K-9 Mail ne prend pas en charge les comptes Hotmail et, comme d\'autres clients, a quelques ennuis avec les serveurs Microsoft Exchange. + \n + \n Si vous avez des suggestions, découvert des bugs ou simplement une idée de fonctionnalité, n\'hésitez pas à vous rendre ici\u00A0: http://k9mail.googlecode.com/ (en anglais) + + + + Version\u00A0: %s + Activer le journal de déboguage + Journaliser les informations de diagnostic supplémentaires + Journaliser les informations personnelles + Vos mots de passe pourront se retrouver dans le journal + + K-9 Mail pour Android + + Tous les messages + Messages récents de tous les comptes + Toutes les boîtes de réception + %s:%s %s %s %s %s - Charger plus de messages - Pour\u00A0: %s - Supprimer - Marquer comme lu - Marquer comme non lu - Ajouter une étoile - Enlever l\'étoile + Charger plus de messages + Pour\u00A0: %s + Supprimer + Marquer comme lu + Marquer comme non lu + Ajouter une étoile + Enlever l\'étoile - Pour - Cc - Cci - Objet - Texte du message + Pour + Cc + Cci + Objet + Texte du message -------- Message d\'origine -------- Objet\u00A0: + Envoyé: De\u00A0: À\u00A0: Cc\u00A0: - \n%s a écrit\u00A0:\n\n - Texte cité - Vous devez ajouter au moins un destinataire. + \n%s a écrit\u00A0:\n\n + Texte cité + Vous devez ajouter au moins un destinataire. Aucune adresse e-mail trouvée - Certaines pièces jointes n\'ont pas été téléchargées. Elles seront téléchargées automatiquement avant l\'envoi de ce message. - Certaines pièces jointes ne peuvent pas être transmises car elles n\'ont pas été téléchargées. + Certaines pièces jointes n\'ont pas été téléchargées. Elles seront téléchargées automatiquement avant l\'envoi de ce message. + Certaines pièces jointes ne peuvent pas être transmises car elles n\'ont pas été téléchargées. Citer le message + De\u00A0: %s <%s> + Pour\u00A0: + Cc\u00A0: + Ouvrir + Enregistrer + \u25BC + \u25B2 + Archiver + Déplacer + Spam + MMM jj aaaa hh:mm a + Pièce jointe enregistrée sur la carte SD en tant que %s. + Impossible d\'enregistrer la pièce jointe sur la carte SD. + Sélectionnez «\u00A0Afficher les images\u00A0» pour afficher les images intégrées. + Afficher les images + Récupération de la pièce jointe. + Impossible de trouver un visualisateur pour %s. - De\u00A0: %s <%s> - Pour\u00A0: - Cc\u00A0: - Ouvrir - Enregistrer - \u25BC - \u25B2 - Archiver - Déplacer - Spam - MMM jj aaaa hh:mm a - Pièce jointe enregistrée sur la carte SD en tant que %s. - Impossible d\'enregistrer la pièce jointe sur la carte SD. - Sélectionnez «\u00A0Afficher les images\u00A0» pour afficher les images intégrées. - Afficher les images - Récupération de la pièce jointe. - Impossible de trouver un visualisateur pour %s. + Télécharger le message complet - Télécharger le message complet + + Les entêtes n\'ont pas toutes été téléchargées ou enregistrées. Sélectionnez «\u00A0Enregistrer toutes les entêtes localement\u00A0» dans les paramètres du serveur entrant pour activer cette fonction. + Toutes les entêtes ont été téléchargées, mais il n\'y en a pas d\'autres à montrer. + La récupération d\'entêtes supplémentaires de la base de données ou du serveur a échouée. - Les entêtes n\'ont pas toutes été téléchargées ou enregistrées. Sélectionnez «\u00A0Enregistrer toutes les entêtes localement\u00A0» dans les paramètres du serveur entrant pour activer cette fonction. - Toutes les entêtes ont été téléchargées, mais il n\'y en a pas d\'autres à montrer. - La récupération d\'entêtes supplémentaires de la base de données ou du serveur a échouée. - Dossiers - Nouveau dossier - Nouveau nom de dossier - (Push) - Plus de cet expéditeur - Message copié - Message déplacé - Message supprimé - Message abandonné - Message enregistré comme brouillon - Le message n\'a pû être supprimé - À propos de %s - Version\u00A0: %s - Étoiles des messages - Les étoiles indiquent des messages suivis - Cases à cocher - Toujours afficher les cases à cocher - Interface conviviale au toucher - Fournit plus d\'espace dans les éléments des listes + Dossiers + Nouveau dossier + + Nouveau nom de dossier + + (Push) + + Plus de cet expéditeur + + Message copié + Message déplacé + Message supprimé + Message abandonné + Message enregistré comme brouillon + Le message n\'a pû être supprimé + + À propos de %s + Version\u00A0: %s + + Étoiles des messages + Les étoiles indiquent des messages suivis + Cases à cocher + Toujours afficher les cases à cocher + Interface conviviale au toucher + Fournit plus d\'espace dans les éléments des listes Lignes dʼaperçu Afficher le nom des correspondants Afficher le nom des correspondants plutôt que leurs adresses e-mail - Utiliser les noms du carnet d\'adresses Utiliser en priorité les noms du carnet d\'adresses Couleur des contacts connus Ne pas mettre en évidence les contacts connus Mettre en évidence (couleur) les contacts connus - Polices à taille fixe - Utiliser une police à taille fixe pour les messages en texte brut - Retour à la liste après suppression - Retourner à la liste de messages après la suppression d\'un message + Polices à taille fixe + Utiliser une police à taille fixe pour les messages en texte brut + Retour à la liste après suppression + Retourner à la liste de messages après la suppression d\'un message + Voir le message suivant après suppression + Voir le message suivant par défaut après suppression Confirmer les actions Demander une confirmation pour chacune des actions sélectionnées @@ -326,193 +370,216 @@ Notifications confidentielles Ne pas afficher les sujets des e-mails dans la barre de notification quand le système est verrouillé - Heures silencieuses Désactiver les sons, vibrations et éclairage DEL aux heures programmées Début des heures silencieuses Fin des heures silencieuses + Configurer la messagerie + Saisissez l\'adresse de messagerie de votre compte\u00A0: + (Vous pouvez ajouter %d comptes supplémentaires.) + Adresse e-mail + %s n\'est pas une adresse e-mail valide. + %s est déjà ajouté. + Mot de passe + Envoyer les messages de ce compte par défaut. + Configuration manuelle - Configurer la messagerie - Saisissez l\'adresse de messagerie de votre compte\u00A0: - (Vous pouvez ajouter %d comptes supplémentaires.) - Adresse e-mail - %s n\'est pas une adresse e-mail valide. - %s est déjà ajouté. - Mot de passe - Envoyer les messages de ce compte par défaut. - Configuration manuelle - - Récupération des informations de compte\u2026 - Vérification des paramètres de serveur entrant\u2026 - Vérification des paramètres de serveur sortant\u2026 + + Récupération des informations de compte\u2026 + Vérification des paramètres de serveur entrant\u2026 + Vérification des paramètres de serveur sortant\u2026 Authentification\u2026 Récupération des paramètres du compte\u2026 - Finition\u2026 - Annulation\u2026 - Presque terminé\u00A0! - Votre compte est configuré et les messages sont en route\u00A0! - Donnez un nom à ce compte (optionnel)\u00A0: - Saisissez votre nom (s\'affiche sur les messages sortants)\u00A0: - Votre compte est configuré\u00A0!\n\nRécupération des messages\u2026 - Ajouter un nouveau compte de messagerie - Quel type de compte est-ce\u00A0? - Compte POP3 - Compte IMAP - Exchange (WebDAV) - Paramètres de serveur entrant - Nom d\'utilisateur - Mot de passe - Serveur POP3 - Serveur IMAP - Serveur Exchange (WebDAV) - Port - Type de sécurité - Type d\'authentification - Aucun - SSL (si disponible) - SSL (toujours) - TLS (si disponible) - TLS (toujours) - Supprimer les messages du serveur\u00A0: - Jamais - Après 7 jours - Lorsque je supprime de la boîte de réception - Marquer comme lu sur le serveur - Utiliser la compression sur le réseau\u00A0: - Réseau mobile - Wi-Fi - Autre - Téléchargement des entêtes de messages - Enregistrer toutes les entêtes localement + Finition\u2026 + Annulation\u2026 + + Presque terminé\u00A0! + Votre compte est configuré et les messages sont en route\u00A0! + Donnez un nom à ce compte (optionnel)\u00A0: + Saisissez votre nom (s\'affiche sur les messages sortants)\u00A0: + + Votre compte est configuré\u00A0!\n\nRécupération des messages\u2026 + + Ajouter un nouveau compte de messagerie + Quel type de compte est-ce\u00A0? + Compte POP3 + Compte IMAP + Exchange (WebDAV) + + Paramètres de serveur entrant + Nom d\'utilisateur + Mot de passe + Serveur POP3 + Serveur IMAP + Serveur Exchange (WebDAV) + Port + Type de sécurité + Type d\'authentification + Aucun + SSL (si disponible) + SSL (toujours) + TLS (si disponible) + TLS (toujours) + + Supprimer les messages du serveur\u00A0: + Jamais + Après 7 jours + Lorsque je supprime de la boîte de réception + Marquer comme lu sur le serveur + + Utiliser la compression sur le réseau\u00A0: + Réseau mobile + Wifi + Autre + + Téléchargement des entêtes de messages + Enregistrer toutes les entêtes localement Stockage externe (carte SD) Stockage interne Stockage additionnel %1$s Emplacement du stockage - Élimination des messages - Immédiatement après avoir supprimé ou déplacé - Pendant chaque récupération - Seulement manuellement - Préfixe de chemin IMAP - Optionnel - Nom du dossier des brouillons - Nom du dossier des envoyés - Nom du dossier de la corbeille - Nom du dossier des archives - Nom du dossier des spams - Afficher seulement les dossiers auxquels je suis abonné - Ouvrir automatiquement le dossier - Chemin OWA - Optionnel - Chemin d\'authentification - Optionnel - Alias du compte - Optionnel - Paramètres du serveur sortant - Serveur SMTP - Port - Type de sécurité - Aucune - SSL - TLS (si disponible) - TLS (toujours) - Authentification requise - Nom d\'utilisateur - Mot de passe - Type d\'authentification - Nom d\'utilisateur et mot de passe - Nom d\'utilisateur - Mot de passe - POP avant SMTP - IMAP avant SMTP - WebDAV (Exchange) avant SMTP - Paramétrage incorrect\u00A0: %s - Options du compte - Compacter - Vider les messages (Danger\u00A0!) - Recréer les données (Dernier recours\u00A0!) - Fréquence de vérification de la messagerie - Jamais - Toutes les minutes - Toutes les 5 minutes - Toutes les 10 minutes - Toutes les 15 minutes - Toutes les 30 minutes - Toutes les heures - Toutes les 2 heures - Toutes les 3 heures - Toutes les 6 heures - Toutes les 12 heures - Toutes les 24 heures - Effectuer une récupération lors d\'une connexion Push - Activer le Push sur ce compte - Si supporté par le serveur, les messages apparaîtront instantanément. Cette option peut considérablement améliorer ou dégrader les performances - Rafraîchir la connexion IDLE - Toutes les minutes - Toutes les 2 minutes - Toutes les 3 minutes - Toutes les 6 minutes - Toutes les 12 minutes - Toutes les 24 minutes - Toutes les 36 minutes - Toutes les 48 minutes - Toutes les 60 minutes - Envoyer les messages de ce compte par défaut - Me notifier à l\'arrivée de messages - M\'avertir pendant la vérification des messages - Nombre de messages à afficher - 10 messages - 25 messages - 50 messages - 100 messages - 250 messages - 500 messages - 1000 messages - Tous + Élimination des messages + Immédiatement après avoir supprimé ou déplacé + Pendant chaque récupération + Seulement manuellement - Impossibilité de copier ou déplacer un message qui n\'est pas synchronisé aver le serveur - La configuration n\'a pu terminer - Nom d\'utilisateur ou mot de passe incorrect.\n(%s) - Impossible de se connecter de manière sûre au serveur.\n(%s) - Impossible de se connecter au serveur.\n(%s) - Modifier les détails - Continuer + Préfixe de chemin IMAP + Optionnel + + Nom du dossier des brouillons + Nom du dossier des envoyés + Nom du dossier de la corbeille + Nom du dossier des archives + Nom du dossier des spams + + Afficher seulement les dossiers auxquels je suis abonné + Ouvrir automatiquement le dossier + + Chemin OWA + Optionnel + + Chemin d\'authentification + Optionnel + Alias du compte + Optionnel + + Paramètres du serveur sortant + Serveur SMTP + Port + Type de sécurité + Aucune + SSL + TLS (si disponible) + TLS (toujours) + Authentification requise + Nom d\'utilisateur + Mot de passe + Type d\'authentification + + Nom d\'utilisateur et mot de passe + Nom d\'utilisateur + Mot de passe + POP avant SMTP + IMAP avant SMTP + WebDAV (Exchange) avant SMTP + + Paramétrage incorrect\u00A0: %s + + Options du compte + + Compacter + Vider les messages (Danger\u00A0!) + Recréer les données (Dernier recours\u00A0!) + + Fréquence de vérification de la messagerie + + Jamais + Toutes les minutes + Toutes les 5 minutes + Toutes les 10 minutes + Toutes les 15 minutes + Toutes les 30 minutes + Toutes les heures + Toutes les 2 heures + Toutes les 3 heures + Toutes les 6 heures + Toutes les 12 heures + Toutes les 24 heures + + Effectuer une récupération lors d\'une connexion Push + Activer le Push sur ce compte + Si supporté par le serveur, les messages apparaîtront instantanément. Cette option peut considérablement améliorer ou dégrader les performances + Rafraîchir la connexion IDLE + Toutes les minutes + Toutes les 2 minutes + Toutes les 3 minutes + Toutes les 6 minutes + Toutes les 12 minutes + Toutes les 24 minutes + Toutes les 36 minutes + Toutes les 48 minutes + Toutes les 60 minutes + + Envoyer les messages de ce compte par défaut + Me notifier à l\'arrivée de messages + M\'avertir pendant la vérification des messages + + + Nombre de messages à afficher + 10 messages + 25 messages + 50 messages + 100 messages + 250 messages + 500 messages + 1000 messages + Tous + + Impossibilité de copier ou déplacer un message qui n\'est pas synchronisé aver le serveur + + La configuration n\'a pu terminer + Nom d\'utilisateur ou mot de passe incorrect.\n(%s) + Impossible de se connecter de manière sûre au serveur.\n(%s) + Impossible de se connecter au serveur.\n(%s) + Modifier les détails + Continuer Paramètres Push - Paramètres du compte - Compte par défaut - Compte par défaut - Envoyer les messages de ce compte par défaut - Nouveaux e-mails - Afficher une notification dans la barre d\'état à la réception d\'un e-mail - Synchronisation - Afficher une notification dans la barre d\'état à la vérification de nouveaux messages - Votre adresse e-mail - Afficher la boîte de réception combinée - Messages dont je suis l\'expéditeur - Afficher une notification dans la barre d\'état même lors d\'un envoi à partir d\'une identité reliée à un compte - Ouvrir message non lu via notification - Recherche des messages non lus après un accès à partir des notifications + Paramètres du compte + Compte par défaut + Compte par défaut + Envoyer les messages de ce compte par défaut + Nouveaux e-mails + Afficher une notification dans la barre d\'état à la réception d\'un e-mail + Synchronisation + Afficher une notification dans la barre d\'état à la vérification de nouveaux messages + Votre adresse e-mail + Afficher la boîte de réception combinée + Messages dont je suis l\'expéditeur + Afficher une notification dans la barre d\'état même lors d\'un envoi à partir d\'une identité reliée à un compte + Ouvrir message non lu via notification + Recherche des messages non lus après un accès à partir des notifications Afficher le nombre de messages non lus Afficher le nombre de messages non lus dans la barre de notification - Défiler les boutons de navigation - Jamais - Lorsque le clavier est disponible - Toujours + Défiler les boutons de navigation + Jamais + Lorsque le clavier est disponible + Toujours - Activer les boutons de déplacement - Afficher les boutons Archiver, Déplacer ou Spam - Défiler les boutons de déplacement + Activer les boutons de déplacement + Afficher les boutons Archiver, Déplacer ou Spam + Défiler les boutons de déplacement - Afficher automatiquement les images - Jamais - Contacts uniquement - Toujours + Afficher automatiquement les images + Jamais + Contacts uniquement + Toujours - Composition de messages + Composition de messages Citer le message original Inclure le message d\'origine dans la réponse @@ -524,294 +591,337 @@ HTML (formattage et images conservés) Text brut (formattage et images omis) + Accusé de réception + Toujours demander un accusé de réception + Style de citation Préfixe (Gmail, Pine) En-tête (Outlook, Yahoo!, Hotmail) Paramètres généraux Affichage - Synchronication de dossiers - Dossiers - Listage de messages - Visualisation de messages - Préfixe de citation - Cryptographie - Fournisseur OpenPGP - Aucun - Non disponible - Signature automatique - Utiliser l\'adresse e-mail du compte pour déduire la clé de signature + Synchronication de dossiers + Dossiers + Listage de messages + Visualisation de messages + Préfixe de citation + Cryptographie + Fournisseur OpenPGP + Aucun + Non disponible + Signature automatique + Utiliser l\'adresse e-mail du compte pour déduire la clé de signature - Fréquence de vérification du dossier - Fréquence de vérification pour 2ème classe + Fréquence de vérification du dossier + Fréquence de vérification pour 2ème classe Stockage + Couleur du compte + Choisir la couleur du compte tel qu\'affichée dans les listes de dossiers ou de comptes - Couleur du compte - Choisir la couleur du compte tel qu\'affichée dans les listes de dossiers ou de comptes - Couleur de la DEL de notification - Choisissez la couleur de clignotement de la DEL de votre téléphone - Nombre de messages à afficher + Couleur de la DEL de notification + Choisissez la couleur de clignotement de la DEL de votre téléphone - Télécharger automatiquement le contenu des messages jusqu\'à - 1\u00A0ko - 2\u00A0ko - 4\u00A0ko - 8\u00A0ko - 16\u00A0ko - 32\u00A0ko - 64\u00A0ko - 128\u00A0ko - 256\u00A0ko - 512\u00A0ko - 1\u00A0Mo - 2\u00A0Mo - Aucune limite + Nombre de messages à afficher - Âge maximal des messages - Toutes dates - Aujourd\'hui - 2 jours - 3 jours - la dernière semaine - les dernières 2 semaines - les dernières 3 semaines - le dernier mois - les derniers 2 mois - les derniers 3 mois - les derniers 6 mois - la dernière année - Dossiers à afficher - Tous - Seulement dossiers de 1ère classe - Dossiers de 1ère et 2ème classes - Tous sauf les dossiers de 2ème classe - Dossiers à vérifier en récupération - Tous - Seulement dossiers de 1ère classe - Dossiers de 1ère et 2ème classes - Tous sauf les dossiers de 2ème classe - Aucun - Dossiers à vérifier avec Push - Tous - Seulement dossiers de 1ère classe - Dossiers de 1ère et 2ème classes - Tous sauf les dossiers de 2ème classe - Aucun - Dossiers de destination déplacer/copier - Tous - Seulement dossiers de 1ère classe - Dossiers de 1ère et 2ème classes - Tous sauf les dossiers de 2ème classe - Synchroniser les suppression à distance - Supprimer les messages lorsqu\'effacés du serveur - Paramètres du dossier - Afficher dans le groupe principal - Afficher dans le haut de la liste de dossiers - Classe d\'affichage du dossier - Aucune - 1ère classe - 2ème classe - Classe de synchronisation du dossier - Aucune - 1ère classe - 2ème classe - Pareille à la classe d\'affichage - Classe Push du dossier - Aucune - 1ère classe - 2ème classe - Pareille à la classe de synchronisation - Paramètres entrants - Configurer le serveur de messagerie entrant - Paramètres sortants - Configurer le serveur de messagerie sortant - Ajouter un autre compte - Nom du compte - Votre nom - Paramètres de notification - Sonner lorsqu\'un message arrive - Vibreur - Vibrer à la réception d\'un e-mail - Type de vibration - Défauts - Type 1 - Type 2 - Type 3 - Type 4 - Type 5 - Nombre de vibrations - Sonnerie + Télécharger automatiquement le contenu des messages jusqu\'à + 1\u00A0ko + 2\u00A0ko + 4\u00A0ko + 8\u00A0ko + 16\u00A0ko + 32\u00A0ko + 64\u00A0ko + 128\u00A0ko + 256\u00A0ko + 512\u00A0ko + 1\u00A0Mo + 2\u00A0Mo + 5Mo + 10Mo + Aucune limite + Âge maximal des messages + Toutes dates + Aujourd\'hui + 2 jours + 3 jours + la dernière semaine + les dernières 2 semaines + les dernières 3 semaines + le dernier mois + les derniers 2 mois + les derniers 3 mois + les derniers 6 mois + la dernière année + + Dossiers à afficher + Tous + Seulement dossiers de 1ère classe + Dossiers de 1ère et 2ème classes + Tous sauf les dossiers de 2ème classe + + Dossiers à vérifier en récupération + Tous + Seulement dossiers de 1ère classe + Dossiers de 1ère et 2ème classes + Tous sauf les dossiers de 2ème classe + Aucun + + Dossiers à vérifier avec Push + Tous + Seulement dossiers de 1ère classe + Dossiers de 1ère et 2ème classes + Tous sauf les dossiers de 2ème classe + Aucun + + Dossiers de destination déplacer/copier + Tous + Seulement dossiers de 1ère classe + Dossiers de 1ère et 2ème classes + Tous sauf les dossiers de 2ème classe + + Synchroniser les suppression à distance + Supprimer les messages lorsqu\'effacés du serveur + + Paramètres du dossier + + Afficher dans le groupe principal + Afficher dans le haut de la liste de dossiers + + Classe d\'affichage du dossier + Aucune + 1ère classe + 2ème classe + + Classe de synchronisation du dossier + Aucune + 1ère classe + 2ème classe + Pareille à la classe d\'affichage + + Classe Push du dossier + Aucune + 1ère classe + 2ème classe + Pareille à la classe de synchronisation + + Paramètres entrants + Configurer le serveur de messagerie entrant + Paramètres sortants + Configurer le serveur de messagerie sortant + Ajouter un autre compte + Nom du compte + Votre nom + Paramètres de notification + Sonner lorsqu\'un message arrive + Vibreur + Vibrer à la réception d\'un e-mail + Type de vibration + Défauts + Type 1 + Type 2 + Type 3 + Type 4 + Type 5 + Nombre de vibrations + Sonnerie Clignotement DEL Clignotement de la DEL quand un message arrive - Paramètres du serveur - Options de composition - Défauts pour la composition - Choisir vos champs De, Cci et signature par défaut - Gérer les identités - Créer des adresses «\u00A0De\u00A0» et signatures alternatives - Gérer les identités - Gérer l\'identité - Éditer l\'identité - Nouvelle identité - Envoyer une copie de tous les messages à - Envoyer une copie de tous les messages envoyés à cette adresse - Éditer - Monter - Descendre - Monter en haut / mettre par défaut - Enlever - Description de l\'identité - (Optionnel) - Votre nom - (Optionnel) - Adresse e-mail - (Requise) - Adresse de réponse - (Optionnel) - Signature - (Optionnel) - Utiliser la signature - Signature - Ajouter une signature à chaque message envoyé - -- \nEnvoyé de mon téléphone Android avec K-9 Mail. Excusez la brièveté. - Identité initiale - Choisir l\'identité - Choisir l\'identité - Choisir le compte/l\'identité + Paramètres du serveur + + Options de composition + Défauts pour la composition + Choisir vos champs De, Cci et signature par défaut + + Gérer les identités + Créer des adresses «\u00A0De\u00A0» et signatures alternatives + + Gérer les identités + + Gérer l\'identité + + Éditer l\'identité + Nouvelle identité + + Envoyer une copie de tous les messages à + Envoyer une copie de tous les messages envoyés à cette adresse + + Éditer + Monter + Descendre + Monter en haut / mettre par défaut + Enlever + + Description de l\'identité + (Optionnel) + Votre nom + (Optionnel) + Adresse e-mail + (Requise) + Adresse de réponse + (Optionnel) + Signature + (Optionnel) + + Utiliser la signature + Signature + Ajouter une signature à chaque message envoyé + + -- \nEnvoyé de mon téléphone Android avec K-9 Mail. Excusez la brièveté. + Identité initiale + Choisir l\'identité + Choisir l\'identité + Choisir le compte/l\'identité Envoyer en tant que - Aller dans Paramètres du compte > Gérer les identités pour créer des identités - Vous ne pouvez votre seule identité - Vous ne pouvez pas utiliser une identité sans adresse e-mail - Votre choix d\'identité et vos changements de signature ne seront pas enregistrés avec un brouillon - Plus ancient en premier - Plus récents en premier - A-Z par expéditeur - Z-A par expéditeur - A-Z par sujet - Z-A par sujet - Messages avec étoiles en premier - Messages sans étoiles en premier - Messages non lus en premier - Messages lus en premier - Messages avec pièces jointes en premier - Messages sans pièces jointes en premier - Trier... - Date - Expéditeur - Sujet - Étoile - Lu/Non lu - Pièces jointes - %s - Supprimer - Le compte «\u00A0%s\u00A0» sera supprimé de K-9 Mail. - Recréer compte - Toutes les données du compte «\u00A0%s\u00A0» vont être effacées de K-9 Mail, mais les paramètres seront conservés. - Vider le compte - Tous les messages du compte «\u00A0%s\u00A0» vont être effacés de K-9 Mail, mais les paramètres seront conservés. + Aller dans Paramètres du compte > Gérer les identités pour créer des identités + Vous ne pouvez votre seule identité + Vous ne pouvez pas utiliser une identité sans adresse e-mail + Votre choix d\'identité et vos changements de signature ne seront pas enregistrés avec un brouillon - Seuls certains comptes «\u00A0Plus\u00A0» autorisent l\'accès POP - permettant à ce programme de se connecter. Si vous ne pouvez pas - vous connecter avec votre adresse électronique et votre mot de passe, - il est possible que vous n\'ayez pas payé un compte «\u00A0Plus\u00A0». Veuillez - lancer le navigateur Web pour accéder à ces comptes de messagerie. + Plus ancient en premier + Plus récents en premier + A-Z par expéditeur + Z-A par expéditeur + A-Z par sujet + Z-A par sujet + Messages avec étoiles en premier + Messages sans étoiles en premier + Messages non lus en premier + Messages lus en premier + Messages avec pièces jointes en premier + Messages sans pièces jointes en premier + Trier... + Date + Expéditeur + Sujet + Étoile + Lu/Non lu + Pièces jointes + %s + + Supprimer + Le compte «\u00A0%s\u00A0» sera supprimé de K-9 Mail. + + Recréer compte + Toutes les données du compte «\u00A0%s\u00A0» vont être effacées de K-9 Mail, mais les paramètres seront conservés. + + Vider le compte + Tous les messages du compte «\u00A0%s\u00A0» vont être effacés de K-9 Mail, mais les paramètres seront conservés. + + Seuls certains comptes «\u00A0Plus\u00A0» autorisent l\'accès POP + permettant à ce programme de se connecter. Si vous ne pouvez pas + vous connecter avec votre adresse électronique et votre mot de passe, + il est possible que vous n\'ayez pas payé un compte «\u00A0Plus\u00A0». Veuillez + lancer le navigateur Web pour accéder à ces comptes de messagerie. Pour utiliser POP3 avec ce fournisseur, vous devez activer POP3 dans les paramètres Yahoo Mail. - Pour utiliser IMAP ou POP3 avec ce fournisseur, veuillez vous assurer que les options IMAP ou POP3 soient activées dans les paramètres de Naver Mail. Pour utiliser IMAP ou POP3 avec ce fournisseur, veuillez vous assurer que les options IMAP ou POP3 soient activées dans les paramètres de Hanmail(Daum) mail. Pour utiliser IMAP ou POP3 avec ce fournisseur, veuillez vous assurer que les options IMAP ou POP3 soient activées dans les paramètres de Paran mail. Pour utiliser IMAP ou POP3 avec ce fournisseur, veuillez vous assurer que les options IMAP ou POP3 soient activées dans les paramètres de Nate mail. - Certificat non reconnu - Accepter le certificat - Rejetter le certificat + Certificat non reconnu + Accepter le certificat + Rejetter le certificat + Suppr (ou D) - Effacer\u000AR - Répondre\u000AA - Répondre à tous\u000AF - Transférer\u000AJ ou P - Message précédent\u000AK, N - Prochain message\u000AM - Déplacer\u000AY - Copier\u000AZ - Zoom arrière\u000AShift-Z - Zoom avant\u000aG - Étoile - Suppr (ou D) - Supprimer\u000AR - + Suppr (ou D) - Supprimer\u000AR - Répondre\u000AA - Répondre à tous\u000AC - Composer\u000AF - Transférer\u000aM - Déplacer\u000AY - Copier\u000AG - Étoile\u000AO - Trier\u000AI - Ordre de tri\u000AQ - Retourner aux dossiers\u000AS - Sélectionner/Désélectionner - - 1 - Afficher seulement les dossiers de 1ère classe \u000A - 2 - Afficher les dossiers de 1ère et 2ème classes\u000A - 3 - Afficher tous les dossiers sauf 2ème classe\u000A - 4 - Afficher tous les dossiers\u000A - Q - Retourner au comptes\u000A - S - Éditer les paramètres du compte - Dossiers - Afficher tous les dossiers - Afficher seulement les dossiers de 1ère classe - Afficher les dossiers de 1ère et 2ème classes - Afficher tous sauf les dossiers de 2ème classe - Emplacement de la signature - Avant la citation - Après la citation - Foncé - Clair - Préférences globales - Déboguage + + + 1 - Afficher seulement les dossiers de 1ère classe \u000A + 2 - Afficher les dossiers de 1ère et 2ème classes\u000A + 3 - Afficher tous les dossiers sauf 2ème classe\u000A + 4 - Afficher tous les dossiers\u000A + Q - Retourner au comptes\u000A + S - Éditer les paramètres du compte + + Dossiers + Afficher tous les dossiers + Afficher seulement les dossiers de 1ère classe + Afficher les dossiers de 1ère et 2ème classes + Afficher tous sauf les dossiers de 2ème classe + + Emplacement de la signature + Avant la citation + Après la citation + Foncé + Clair + Affichage + Préférences globales + Déboguage Confidentialité Réseau - Liste de comptes - Liste de messages - Messages - Thème - Langue + Interaction + Liste de comptes + Liste de messages + Messages + Thème + Langue Disposition colonne unique Réarranger les messages HTML pour les petits écrans Zoom système Activer les widgets zoom ou le pincer-zoomer si supporté par le système + Langue du système + Synchronisation en arrière-plan + Jamais + Toujours + Lorsque «\u00A0Données en arrière-plan\u00A0» est activé + Lorsque «\u00A0Données en arrière-plan\u00A0» et «\u00A0Synchronisation auto\u00A0» sont activés - Langue du système - Synchronisation en arrière-plan - Jamais - Toujours - Lorsque «\u00A0Données en arrière-plan\u00A0» est activé - Lorsque «\u00A0Données en arrière-plan\u00A0» et «\u00A0Synchronisation auto\u00A0» sont activés - Aucun message sélectionné - Format de date - SHORT - MEDIUM - dd-MMM-yyyy - yyyy-MM-dd - Opérations groupées - Supprimer la sélection - Marquer la sélection comme lue - Marquer la sélection comme non lue - Ajouter une étoile à la sélection - Effacer l\'étoile de la sélection - Déplacer vers Archives - Déplacer vers Spams - Déplacer la sélection - Copier la sélection - Mode étoile - Mode sélection - Mode normal - Sélectionner tout - Désélectionner tout - Maximum de dossiers Push à vérifier - 10 dossiers - 25 dossiers - 50 dossiers - 100 dossiers - 250 dossiers - 500 dossiers - 1000 dossiers - Animation - Utiliser des effets visuels criards - Interface gestuelle - Accepter le contrôle par gestuelle + Aucun message sélectionné + + Format de date + + SHORT + MEDIUM + dd-MMM-yyyy + yyyy-MM-dd + + Opérations groupées + Supprimer la sélection + Marquer la sélection comme lue + Marquer la sélection comme non lue + Ajouter une étoile à la sélection + Effacer l\'étoile de la sélection + Déplacer vers Archives + Déplacer vers Spams + Déplacer la sélection + Copier la sélection + Mode étoile + Mode sélection + Mode normal + Sélectionner tout + Désélectionner tout + + Maximum de dossiers Push à vérifier + 10 dossiers + 25 dossiers + 50 dossiers + 100 dossiers + 250 dossiers + 500 dossiers + 1000 dossiers + + Animation + Utiliser des effets visuels criards + Interface gestuelle + Accepter le contrôle par gestuelle Agencement compact Modifier l\'agencement pour afficher plus d\'éléments par page @@ -821,94 +931,115 @@ Visualisation d\'un message Différents écrans à liste - Gérer la touche «\u00A0Retour\u00A0» - La touche «\u00A0Retour\u00A0» remonte toujours un niveau - Boîte de messagerie unifiée au démarrage - Afficher la boîte de messagerie unifiée au démarrage - Afficher la taille du compte - Désactiver pour un affichage plus rapide - Compter les résultats de recherche - Désactiver pour un affichage plus rapide + Gérer la touche «\u00A0Retour\u00A0» + La touche «\u00A0Retour\u00A0» remonte toujours un niveau + + Boîte de messagerie unifiée au démarrage + Afficher la boîte de messagerie unifiée au démarrage + + Afficher la taille du compte + Désactiver pour un affichage plus rapide + + Compter les résultats de recherche + Désactiver pour un affichage plus rapide Cacher les comptes spéciaux Cacher «\u00A0Boîte de messagerie unifiée\u00A0» et «\u00A0Tous les messages\u00A0» - %s %s - - Avec étoile - - Non lu - Tous les messages - Tous les messages dans les dossiers recherchables - Boîte de messagerie unifiée - Tous les messages des dossiers à unifier - Tapoter l\'enveloppe ou l\'étoile pour les messages non lus ou avec étoile - Unifier - Les messages non lus sont affichés dans la boîte de messagerie unifiée - Dossiers dans lesquels chercher - Tous - Affichables - Aucuns - Contrôle à distance de K-9 Mail - Permettre à cette application de contrôler les paramètres et activités de K-9 Mail - Taille de police - Configurer la taille de police - Liste de comptes - Nom du compte - Description du compte - Liste des dossiers - Nom du dossier - Status du dossier - Liste de messages - Sujet du message - Expéditeur du message - Date du message + %s %s + - Avec étoile + - Non lu + + Tous les messages + Tous les messages dans les dossiers recherchables + + Boîte de messagerie unifiée + Tous les messages des dossiers à unifier + + Tapoter l\'enveloppe ou l\'étoile pour les messages non lus ou avec étoile + + Unifier + Les messages non lus sont affichés dans la boîte de messagerie unifiée + + Dossiers dans lesquels chercher + Tous + Affichables + Aucuns + + Contrôle à distance de K-9 Mail + Permettre à cette application de contrôler les paramètres et activités de K-9 Mail + + Taille de police + Configurer la taille de police + + Liste de comptes + Nom du compte + Description du compte + + Liste des dossiers + Nom du dossier + Status du dossier + + Liste de messages + Sujet du message + Expéditeur du message + Date du message Aperçu - Vue du message - Expéditeur du message - Destinataire du message (Pour) - Destinataire du message (CC) - Entêtes supplémentaires - Sujet du message - Heure du message - Date du message - Contenu du message - Plus minuscule - Minuscule - Plus petit - Petit - Moyen - Grand - Plus grand - Plus petit - Petit - Normal - Grand - Plus grand - Cochez «\u00A0Paramètres > Utiliser le contounement du bogue de Gallerie\u00A0» pour être capable d\'attacher des images ou vidéos en utilisant Gallerie 3D. - Utiliser «\u00A0Joindre une image\u00A0» ou «\u00A0Joindre une vidéo\u00A0» pour joindres des images ou vidéos avec Gallerie 3D - Divers - Utiliser le contounement du bogue de Gallerie - Afficher les boutons pour ajouter des images ou vidéos en pièces jointes (pour contourner le bogue de Gallerie 3D) + Vue du message + Expéditeur du message + Destinataire du message (Pour) + Destinataire du message (CC) + Entêtes supplémentaires + Sujet du message + Heure du message + Date du message + Contenu du message - - Aucune application n\'a été trouvée pour cette action. - La version installée de APG n\'est pas supportée. - Signer - Chiffrer - Déchiffrer - Vérifier - «\u00A0inconnu\u00A0» - id\u00A0: %s - K-9 Mail n\'a pas la permission d\'accéder à AGP, veuillez réinstaller K-9 Mail pour corriger cela. - Les messages PGP/MIME ne sont pas encore supportés - Attention\u00A0: La signature et le chiffrement des pièces jointes ne sont pas encore supportés. - Envoi annulé. + Plus minuscule + Minuscule + Plus petit + Petit + Moyen + Grand + Plus grand - Enregistrer le brouillon\u00A0? - Enregistrer ou abandonner ce message\u00A0? + Plus petit + Petit + Normal + Grand + Plus grand - Ce message ne peut être affiché parce que le jeu de caractères «\u00A0%s\u00A0» n\'a pu être trouvé. - Sélectionnez le texte à copier + + Cochez «\u00A0Paramètres > Utiliser le contounement du bogue de Gallerie\u00A0» pour être capable d\'attacher des images ou vidéos en utilisant Gallerie 3D. + + + Utiliser «\u00A0Joindre une image\u00A0» ou «\u00A0Joindre une vidéo\u00A0» pour joindres des images ou vidéos avec Gallerie 3D + + Divers + Utiliser le contounement du bogue de Gallerie + Afficher les boutons pour ajouter des images ou vidéos en pièces jointes (pour contourner le bogue de Gallerie 3D) + + + Aucune application n\'a été trouvée pour cette action. + La version installée de APG n\'est pas supportée. + Signer + Chiffrer + Déchiffrer + Vérifier + «\u00A0inconnu\u00A0» + id\u00A0: %s + K-9 Mail n\'a pas la permission d\'accéder à AGP, veuillez réinstaller K-9 Mail pour corriger cela. + Les messages PGP/MIME ne sont pas encore supportés + Attention\u00A0: La signature et le chiffrement des pièces jointes ne sont pas encore supportés. + Envoi annulé. + + Enregistrer le brouillon\u00A0? + Enregistrer ou abandonner ce message\u00A0? + + Ce message ne peut être affiché parce que le jeu de caractères «\u00A0%s\u00A0» n\'a pu être trouvé. + + Sélectionnez le texte à copier Confirmer la suppression Voulez-vous supprimer ce message\u00A0? @@ -919,17 +1050,17 @@ Voulez-vous déplacer ce message vers le dossiers à spams\u00A0? Voulez-vous déplacer %1$d messages vers le dossiers à spams\u00A0? - + --> Déplacer Ne pas déplacer @@ -938,12 +1069,59 @@ Sortie de deboguage «\u00A0logcat\u00A0» activée + » + Connexion impossible + Paramètres d\'import & d\'export + Exporter les comptes + Exporter paramètres et comptes + Importation + Exportation + Importer les paramètres + Importer la sélection + Paramètres globaux + Exportation des paramètres... + Importation des paramètres... + Balayage des fichiers... + Paramètres exportés vers %s + Paramètres globaux importés depuis %s + Importés %s depuis %s + + 1 compte + %s comptes + + Impossible d\'exporter les paramètres + Impossible d\'importer les paramètres de %s + Exporation réussi + Exporation échoué + Importation réussi + Importation échoué + Activer le compte + Pour pouvoir utiliser le compte \"%s\" vous devez fournir le %s. + + mot de passe serveur + mots de passe serveur + + Serveur entrant (%s): + Serveur sortant (%s): + + Réglage du mot de passe... + Réglages des mots de passe... + + Utilisez le mot de passe du serveur entrant + Activer + + Impossible de gérer le fichier de la version %s + Le compte «\u00A0%s\u00A0» n\'est pas disponible\u00A0; veuillez vérifier le stockage Enregistrer les pièces jointes dans… Enregistrer les pièces jointes Aucun gestionnaire de fichiers trouvé. Où souhaitez-vous enregistrer cette pièce jointe\u00A0? + Déplacer haut + Déplacer bas + Déplacer compte... + diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index b8f9b74dc..3937bc76e 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -1047,6 +1047,47 @@ Benvido á Configuración de K-9. K-9 é un cliente de correo OpenSource para An Imposible conectar. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A conta \"%s\" non está dispoñible; verifica o almacenamento @@ -1055,5 +1096,6 @@ Benvido á Configuración de K-9. K-9 é un cliente de correo OpenSource para An + diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index d75288455..c8e65f3f8 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1050,6 +1050,47 @@ Benvenuto nella configurazione della posta di K-9. K-9 è un client di posta ope Impossibile connettersi. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1058,5 +1099,6 @@ Benvenuto nella configurazione della posta di K-9. K-9 è un client di posta ope + diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 494e9f839..4d4385371 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -1037,6 +1037,47 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール \u203a 接続できません + 設定のインポートとエクスポート + アカウント設定をエクスポート + 設定とアカウント情報をエクスポート + インポート + エクスポート + 設定をインポート + インポート項目の選択 + グローバル設定 + 設定をエクスポートしています... + 設定をインポートしています... + ファイルを調べています... + 設定を %s に保存しました。 + グローバル設定を %s からインポートしました。 + %s の設定を %s からインポートしました。 + + 1 アカウント + %s アカウント + + 設定のエクスポートが失敗しました。 + %s からのインポートに失敗しました。 + エクスポート成功 + エクスポート失敗 + インポート成功 + インポート失敗 + アカウントを有効化 + アカウント \"%s\" を有効にするために、%s を入力してください + + サーバのパスワード + サーバのパスワード + + 受信メールサーバ (%s): + 送信メールサーバ (%s): + + パスワードを設定... + パスワードを設定... + + 受信メールサーバパスワードを利用 + 有効 + + バージョン %sのファイルは利用できません + アカウント \"%s\" は利用できません。ストレージを確認してください。 添付ファイルの保存先 @@ -1045,5 +1086,6 @@ K-9 Mail セットアップにようこそ。\nK-9 は標準のAndroidメール 上に移動 下に移動 + アカウントを移動しています diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 25a65169a..36af52ee2 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -1048,6 +1048,47 @@ K-9 메일 설치를 환영합니다. K-9은 표준 안드로이드 메일 클 연결실패 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \"%s\" 계정을 이용할 수 없습니다.; 저장장치를 확인하십시오. 첨부파일 저장위치 @@ -1056,5 +1097,6 @@ K-9 메일 설치를 환영합니다. K-9은 표준 안드로이드 메일 클 위로 아래로 + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 7d3ae4106..7581b445e 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -1046,6 +1046,47 @@ Welkom bij K-9 Mail setup. K-9 is een open source mail cliënt voor Android, ge Kan geen verbinding maken. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Account \"%s\" is niet beschikbaar; controleer opslag Sla bijlagen op naar... @@ -1054,5 +1095,6 @@ Welkom bij K-9 Mail setup. K-9 is een open source mail cliënt voor Android, ge + diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 5734c4cdc..7e6d4feaa 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -1059,6 +1059,47 @@ Witaj w K-9 Mail, darmowym programie pocztowym dla systemu Android. Najistotniej + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Konto \"%s\" jest niedostępne; sprawdź pamięc @@ -1067,5 +1108,6 @@ Witaj w K-9 Mail, darmowym programie pocztowym dla systemu Android. Najistotniej + diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index eb4d22b7b..007a892af 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -1043,6 +1043,47 @@ Bem-vindo à configuração do K-9 Mail. K-9 é um cliente de e-mail com código Não é possível conectar. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1051,5 +1092,6 @@ Bem-vindo à configuração do K-9 Mail. K-9 é um cliente de e-mail com código + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 128e30a42..95643bfeb 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -1039,6 +1039,47 @@ Не получается соединиться. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Учётная запись\"%s\" недоступна; проверьте хранилище Сохранять вложения в... @@ -1047,5 +1088,6 @@ + diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index bab518521..9902f74a5 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -1049,6 +1049,47 @@ Välkommen till installationen av K-9 E-post. K-9 är en e-postklient med öppen Kan inte ansluta. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1057,5 +1098,6 @@ Välkommen till installationen av K-9 E-post. K-9 är en e-postklient med öppen + diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml new file mode 100644 index 000000000..e3c5ad51d --- /dev/null +++ b/res/values-tr/strings.xml @@ -0,0 +1,1074 @@ + + + K-9 Posta + K-9 Mail BETA + Yazarlar: %s + Revizyon Bilgileri: %s + K-9 posta kaynak kodları içerir: %s + + Eposta eklerini oku + Bu uygulamaya Eposta eklerinizi okumasına izin ver. + Epostaları oku + Bu uygulamaya Epostalarınızı okumaya izin ver. + Epostaları sil + Bu uygulamaya Epostalarınızı silmeye izin ver. + + Hakkında %s + Hesaplar + Gelişmiş + %s + K-9 Hesaplar + + %s:%s + + Oluştur + Onar + Klasör Seç + Bir renk Seç + + %s%s%s + + \u0020[%d] + \u0020(Poll %s%s%s) + \u0020(Fetching headers %s:%s%s) + \u0020(Sending %s%s) + \u0020(Proc %s%s%s) + \u0020%s/%s + + \u0020(Next poll @ %s) + \u0020(Sync disabled) + + + İleri + Geri + Tamam + İptal + Gönder + Tekrar Gönder + Seç + Kaldır + Cevap + Hepsini Cevapla + Sil + Arşiv + Spam + Klasörü temizle + İlet + Taşı + Devam + Tamam + Kaldır + Çıkart + Taslak olarak Kaydet + Yeniden dene + Yinele + Epostayı Kontrol et + Mesajları Gönder + Klasör Listesi + Klasörleri Yinele + Hepsini Okundu olarak işaretle + Hesap Ekle + Oluştur + Arama + Arama Sonuçları + Ayarlar + Açık + Hesap Ayarları + Klasör Ayarları + Genel Ayarlar + Hesabı Kaldır + Bekleyen faaliyetleri Temizle (Tehlike!) + + Hesaplar + + + Oku + Okundu olarak işaretle + Paylaş + Gönderici Seç + + Hepsini okundu olarak işaretle + içindeki bütün mesajları \'%s\' okundu olarak işaretle? (Klasöre + eklenen mesajlar K-9 de gösterilemez.) + + Yıldız Ekle + Yıldızı Kaldır + Kopyala + Tam Başlığı Göster + Tam Başlığı Sakla + Metin Seç + + Okunmadı Olarak İşaretle + Taşı + Klasörler + Ayrıntıları Göster/Sakla + Ekle Cc/Bcc + Konuyu Düzenle + Alındıyı Oku + Okudu alındısı istenecek + Okudu alındısı istenmeyecek + Ekle + Ekle (İmaj) + Ekle (Video) + Çöp Ayarları + Çöpü Boşalt + Silmek + Yerel mesajları temizle + Sıralamayı Seç + Sıralamayı tersine çevir + Hakkında + + Ayarlar + Hesap Seçenekleri + Klasör Seçenekleri + + (Konu yok) + Tarih yok + Gönderen yok + Seçim + (Poll %s%s) + Yüklenen mesajlar\u2026 + Bağlantı hatası + Mesaj bulunamadı + Hata + Gönderiliyor + + Birden fazla mesaj yüklemeyi deniyor + + Boyuta kadar yükle + %d daha fazla + + GB + MB + KB + B + + + Hesap \"%s\" den küçüldü + %s + to + %s + + + Hesap sıkıştırma \"%s\" + Hesap temizleme \"%s\" + Hesap yenileme \"%s\" + + Yeni posta + den yeni posta %s + %d Okunmadı (%s) + %d Yeni mesajlar (%s) + in %d hesaplar + Mesaj gönderilemedi + + Posta kontrol ediliyor: %s:%s + Posta kontrol ediliyor + Posta gönderiliyor: %s + Posta gönderiliyor + : + + Gelen Kutusu + Giden Kutusu + + Taslaklar + Çöp + Gönderilen + Arşiv + Spam + + + %s (Taslaklar) + %s (Çöp) + %s (Gönder) + %s (Arşiv) + %s (Spam) + + Bazı mesajların gönderilmesi başarısız + Bakın %s klasörüne detaylar için. + K-9 mesajlarınızın bir kısmını gönderirken bir sorunla karşılaştı. + Ancak, sorunun yapısına uygun olarak, K-9 mesajların gidip gitmediğinden emin olamamakta, + Alıcılar çoktan mesajın kopyalarını almış da olabilir. + \u000a\u000a Mesajlar bu tip sorunlar çıkarttığı için Giden kutusunda şimdi yıldızla işaretlendi. + Yıldızları kaldırırsanız, K-9 mesajları tekrar göndermeyi dener. + Giden kutusuna uzun basın \"Giden mesajlar\" eylemini sırayla diğer gönderme girişimlerini başlatmayı bulmak için.\u000A\u000a + %s klasör başarısızlıklarla ilgili hata mesajları içerebilir. + + K-9 uyarı + Ağın olmaması yüzünden Eşleştirme ve Gönderme durduruldu. + + Daha fazla mesaj yok + + K-9 Posta\'ya Hoşgeldiniz!\n K-9 Android için bir gelişmiş eposta alıcısıdır ki birçok gelişmiş posta sunucusu özelliklerini destekler. Her ağ merkezli iletişim uygulaması gibi, K-9\'i ağ bant genişliğinin bir büyük kısmını kullanmaya yapılandırmak mümkündür. Eğer sınırsız bir data paketiniz yoksa veya uluslararası dolaşıma niyetleniyorsanız, lütfen bant genişliğinizin kullanımına dikkat edin. Bu uygulama olduğu gibi sağlanır, her türlü garanti kapsamı dışında. Bu uygulamanın ne yazar ne de dağıtıcısı herhangi bir davranıştan, kullanımdan, terbiyesizlik veya bu uygulamanın kötüye kullanımından sorumlu tutulamaz. Tüm bunlar söylenildi, biz K-9\'in iyi çalışacağına çok inanıyoruz ve biz sizin onunla bulacağınız hakkındaki her problemi duymak istiyoruz. Lütfen eposta atın support@kaitenmail.com Eğer sorun yaşıyorsanız, doğru şeyler yapmak için elimizden gelenin en iyisini yapacağız. + + Version: %s + Onarma günlüğünü Etkinleştir + Extra tanı bilgisi günlüğü tut + Hassas bigi günlüğü tut + Günlüklerde şifreler görünebilir. + + Android için K-9 Posta + + Bütün Postalar + Bütün hesaplardan gelen yeni mesajlar + Gelen Kutusu bütün postaları + + %s:%s + %s + %s + %s + %s + Daha fazla mesaj yükle + To:%s + Sil + Okundu olarak işaretle + Okunmadı olarak işaretle + Yıldız Ekle + Yıldızı Kaldır + + Kime + Cc + Bcc + Konu + Mesaj Metni + -------- Orjinal Mesaj -------- + Konu: + Gönder: + Kimden: + Alıcı: + Cc: + %s wrote:\n\n + Quoted text + En az bir Alıcı eklemelisiniz. + Eposta adresi bulunamadı. + Bazı ekler indirilemedi.Bu mesaj gitmeden önce onlar otomatik indirilecek. + Bazı ekler iletilemedi çünkü onlar indirilemedi. + Alıntı mesaj + + Kimden: %s <%s> + Kime: + Cc: + Açık + Kaydet + \u25BC + \u25B2 + Arşiv + Taşı + Spam + MMM dd yyyy hh:mm a + SD Karta Ek kaydedildi %s. + SD Karta Ek kaydedilemedi. + Seç \"Resimleri Göster\" gömülü resimleri göster. + Resimleri Göster + Çekici Ek. + için görüntüleyici bulunamadı. %s. + + Bütün mesajı indir. + + + Bütün başlıklar indirilemedi veya kaydedilemedi. Seç \"Bütün başlıkları kaydet\" hesabın gelen sunucu ayarların içinde gelecek için bunu etkinleştirin. + Bütün başlıklar indirildi, fakat göstermeye ek başlıklar yok. + Veritabanından veya posta suncucundan ek başlıkların gerigetirilişi başarısız. + + Klasörler + Yeni Klasör + + Yeni Klasör İsmi + + (Push) + + Bu Göndericiden daha fazla mesaj + + Mesaj kopyalandı + Message taşındı + Message silindi + Message çıkartıldı + Mesaj taslak olarak kaydedildi. + Mesaj silinemedi + + Hakkında %s + Version: %s + + Yıldızları Göster + Yıldızlar bayraklı mesajları gösterir. + Çoklu-seçim işaretkutuları + Çoklu-seçim işaretkutularını Daima göster. + Mesaj önizleme + Mesaj önizleme ile Roomier listesi ürünü. + Önizleme hatları + Mektuplaşanların isimlerini göster. + Mektuplaşanların eposta adresleri yerine isimlerini göster. + Rehberdeki kişi isimlerini göster. + Alıcı isimlerini mümkün olduğunda rehberdeki isimlerden kullan. + Bağlantıları reklendir + Rehberdeki kişilerin isimlerini renklendirme. + Rehberdeki kişilerin isimlerini renklendir. + + Sabit genişlikli fontlar + Düz metin mesajları gösterdiğinde sabit genişlikli font kullan. + Sildikten sonra listeye dön. + Mesajı sildikten sonra mesaj listesine dön. + + + + Eylemleri Onaylayın + Seçilmiş eylemleri yaptığınız zaman bir diyalog göster + Arşiv + Sil (message view only) + Spam + Hepsini okundu olarak işaretle + Gönder + + Ekran-kilidi bildirimleri + Sistem kilitlendiğinde bilidirim çubuğunda mesaj konusunu gösterme + + Sessiz Zaman + Çalma, gürültü ve geceleyin flaş devre dışı + Sessiz Zaman başlıyor + Sessiz Zaman bitiyor + + Bir yeni hesap kurmak + Bu hesabın eposta adresine giriş: + (Ekleyebilirsiniz %d daha fazla hesap.) + Eposta adresi + %s geçerli bir eposta adresi değil. + %s çoktan eklendi. + Şifre + Postaları varsayılan olarak bu hesaptan gönder + El ile ayar + + + Hesap bilgisi alınıyor\u2026 + Gelen sunucu ayarları kontrol ediliyor\u2026 + Giden sunucu ayarları kontrol ediliyor\u2026 + Kimlik Doğrulaması\u2026 + Çekici hesap ayarları\u2026 + Bitiyor\u2026 + İptal ediliyor\u2026 + + Hemen hemen tamam! + Hesabınız kurulu,ve posta onun yolunda! + Bu hesaba bir isim verin (isteğe bağlı): + İsminizi yazın (Giden mesajlarda görüntülenir): + + Hesabınız kurulu!\n\nFetching mail\u2026 + + Hesap tipi + Bu ne tür bir hesaptır? + POP3 + IMAP + Exchange (WebDAV) + + Gelen sunucu ayarları + Kullanıcı adı + Şifre + POP3 sunucu + IMAP sunucu + Exchange sunucu + Port + Güvenlik tipi + Kimlik doğrulama tipi + Hiçbiri + SSL (Varsa) + SSL (daima) + TLS (Varsa) + TLS (Daima) + + Bir mesaj sildiğim zaman + Sunucudan silme + 7 gün sonra + Sunucudan sil + Sunucuda okundu olarak işaretle + + Ağda sıkıştırma kullan: + Mobile + Wi-Fi + Other + + Başlıkları indir + Bütün mesaj başlıklarını yerel kaydet + + Harici depolama (SD card) + Düzenli dahili depolama + %1$s ek iç depolama + Depolama yeri + + Silinen mesajları çıkart + Hemen + Seçim zamanı + Sadece elle + + IMAP yolu öneki + (Eğer varsa Otomatik NAMESPACE kullanılır) + + Taslaklar klasörü + Giden Klasörü + Çöp Klasörü + Arşiv Klasörü + Spam Klasörü + + Sadece abone olunan klasörleri göster + Otomatik genişleyen klasör + + OWA path + İsteğe bağlı + + Kimlik doğrulama yolu + İsteğe bağlı + Mailbox takma adı + İsteğe bağlı + + Giden sunucu ayarları + SMTP sunucu + Port + Güvenlik Tipi + Hiçbiri + SSL + TLS (Varsa) + TLS (Daima) + Oturum açmayı gerektirir. + Kullanıcı Adı + Şifre + Kimlik Doğrulama Tipi + + Kullanıcı Adı & şifre + Kullanıcı Adı + Şifre + POP before SMTP + IMAP before SMTP + WebDAV (Exchange) before SMTP + + Geçersiz kurulum: %s + + Hesap seçenekleri + + Sıkıştırmak + Mesajları temizle (tehlike!) + Veriyi canlandır (Son çare!) + + Klasör bilgi toplama sıklığı + + Asla + Her Dakika + Her 5 dakikada + Her 10 dakikada + Her 15 dakikada + Her 30 dakikada + Her Saat + Her 2 saatte + Her 3 saatte + Her 6 saatte + Her 12 saatte + Her 24 saatte + + Push için bağlandığında + Push maili bu hesap için etkinleştir. + Eğer sunucunuz onu destekliyorsa, yeni mesajlar anında belirecek. Bu seçenek dramatik olarak performansı arttırabilir veya zarar verebilir. + IDLE bağlantıyı Yenile + Her Dakika + Her 2 dakikada + Her 3 dakikada + Her 6 dakikada + Her 12 dakikada + Her 24 dakikada + Her 36 dakikada + Her 48 dakikada + Her 60 dakikada + + Postayı varsayılan olarak bu hesaptan gönder. + Posta vardığında beni uyar. + Posta kontrol edilirken beni uyar. + + + Görüntülenecek Mesaj sayısı + 10 mesaj + 25 mesaj + 50 mesaj + 100 mesaj + 250 mesaj + 500 mesaj + 1000 mesaj + Bütün mesajlar + + Sunucu ile eşleştirilmemiş bir mesaj kopyalanamaz veya taşınamaz. + + Kurulum bitirilmedi + Kullanıcı Adı veya şifre doğru değil.\n(%s) + Sunucuya güvenli bağlantı kurulamadı.\n(%s) + Sunucuya bağlanılamadı.\n(%s) + Ayrıntıları düzenle + Devam + + Gelişmiş + Hesap ayarları + Varsayılan hesap + Varsayılan hesap + Postayı varsayılan olarak bu hesaptan gönder + Yeni posta bildirimleri + Eşleştirme bildirimleri + Eposta adresiniz + Posta geldiğinde durum çubuğunda bildir + Posta kontrol edildiğinde durum çubuğunda bildir + Birleşik Gelen kutusunu göster + Giden postayı ekle + Gönderdiğim mesajlar için bir bildirim göster + Okunmamış mesaj bildirimi açık + Bildirim açık olduğunda okunmamış mesajlar için arama yap + Okunmamış sayısını göster + Bildirim çubuğunda okunmamış mesaj numarasını göster. + + Gezinme tuşlarını kaydırma + Asla + Klavye olduğu zaman + Daima + + İşaretleme düğmelerini etkinleştir + Arşiv, Taşıma ve Spam düğmelerini göster. + İşaretleme düğmelerini kaydır + + Daima imajları göster + Hayır + Kişilerden + Herkesten + + Posta gönderiliyor + + Cevaplarken orjinal mesajı sakla + Mesajları cevaplarken, orjinal mesaj cevabınızın içinde. + + Alıntı metinden sonra cevapla + Mesajları cevaplarken, orjinal mesaj cevabınızın üzerinde gözükecek. + + Mesaj Biçimi + Düz Metin (imajlar ve biçimler kaldırılacak) + HTML (imajlar ve biçimler korunacak) + + Alındı oku + Daima bir alındı okuma iste + + Alıntı sitili + Önek (Gmail, Pine gibi) + Başlık (Outlook, Yahoo!, Hotmail gibi) + + Genel Ayarlar + Görüntü + Posta alma + Klasörler + Mesajları listeleme + Mesajlara bakmak + Alınan metinden önek + Kriptografi + OpenPGP Sağlayıcı + Hiç + mevcut değil + Otomatik oturum açma + Hesapların eposta adreslerini imza anahtarı sanmayı kullan. + + Klasör veri toplama sıklığı + 2ci sınıf kontrol etme sıklığı + + Depolama + + Hesap rengi + Klasör ve hesap listesinde kullanılan hesap rengini seç + + Bildirim LED ışığı rengi + Cihazınızın LED ışığına renk seçin bu hesap için ışıldasın + + Yerel klasör boyutu + + Mesajları şu boyut üzerinden getir + 1Kb + 2Kb + 4Kb + 8Kb + 16Kb + 32Kb + 64Kb + 128Kb + 256Kb + 512Kb + 1Mb + 2Mb + 5Mb + 10Mb + Her boyutta (sınır yok) + + Mesajları eşleştirme sıklığı + Her zaman (sınır yok) + Bugün + Son 2 gün + Son 3 gün + Son hafta + Son 2 hafta + Son 3 hafta + Son Ay + Son 2 ay + Son 3 ay + Son 6 ay + Geçen yıl + + Görüntülenecek Klasörler + Hepsi + Sadece 1nci Sınıf klasörler + 1nci ve 2nci Sınıf klasörler + 2nci Sınıf klasörler dışında hepsi + + Anket Klasörler + Hepsi + Sadece 1nci Sınıf klasörler + 1st and 2nd Class folders + 2nci Sınıf klasörler dışında hepsi + Hiçbiri + + Push Klasörleri + Hepsi + Sadece 1nci Sınıf klasörler + 1nci ve 2nci Sınıf klasörler + 2nci Sınıf klasörler dışında hepsi + Hiçbiri + + Taşıma/Kopyalama hedef klasörleri + Hepsi + Sadece 1nci Sınıf klasörler + 1nci ve 2nci Sınıfı klasörler + 2nci Sınıf klasörler dışında hepsi + + Sekron sunucusu silme işlemleri + Mesajlar silindiğinde sunucudan kaldır + + Klasör Ayarları + + Zirve grupta göster + Klasör listesinin zirvesine yakın göster + + Klasör Görüntüleme Sınıfı + Hiçbiri + 1nci Sınıf + 2nci Sınıf + + Klasör Senkronize Sınıfı + Hiçbiri + 1nci Sınıf + 2nci Sınıf + Aynı görünüleme sınıfı + + Klasör itme sınıfı + Hiçbiri + 1nci Sınıf + 2nci Sınıf + Aynı senkranizayon sınıfı + + Gelen Sunucu + Gelen posta sunucusunu yapılandır + Giden sunucu + Giden posta (SMTP) sunucusunu yapılandır + Birbaşka hesap ekle + Hesap ismi + İsminiz + Bildirimler + Posta geldiğinde çal + Titre + Posta geldiğinde titre + Titreme modeli + Varsayılan + Model 1 + Model 2 + Model 3 + Model 4 + Model 5 + Titreşimi tekrarla + Yeni posta zilsesi + LED Işığı + Posta geldiğinde LED ışıldasın + + Sunucu Ayarları + + Mesaj komposizyon seçenekleri + Kompozisyon varsayılanları + Sizin varsayılan Kimden, Bcc ve imzayı ayarlayın + + Kimlikleri Yönetmek + \'Kimden\' adresler ve imzaların alternatiflerini ayarlayın + + Kimlikleri Yönetmek + + Kimlik Yönetmek + + Kimlik düzenlemek + Yeni kimlik + + Bcc bütün mesajlar to + Her giden mesajın bir kopyasını bu adrese gönder + + Düzenle + Yukarı hareket + Aşağı hareket + En üste taşı / varsayılan yap + Kaldır + + Kimlik Tanımlama + (İsteğe bağlı) + İsminiz + (İsteğe bağlı) + Eposta adresi + (Gerekli) + Cevap adresi + (İsteğe bağlı) + İmza + (İsteğe bağlı) + + İmza kullan + İmza + Gönderdiğiniz her mesaja bir imza atın + + -- \n Android için K-9 Posta\'dan gönderidi. Lütfen bu kısa notumu mazur görün. + İlk kimlik + Kimlik seç + Kimlik seç + Hesap/Kimlik seç + Olarak gönderin + + Hesap ayarlarına gidin -> Kimlikleri yönetin kimlikleri yaratın + Sadece kimliğinizi kaldıramazsınız + Bir eposta adressiz bir kimlik kullanamazsınız + Kimliğinizin seçimi ve imzası değişimi bir taslak ile kaydedilmeyecek + + En erken mesajlar ilk + En son mesajlar ilk + Göndereni alfabetik sırala + Göndereni ters alfabetik sırala + Konuyu alfabetik sırala + Konuyu ters alfabetik sırala + Yıldızlı mesajlar ilk + Yıldızsız mesajlar ilk + Okunmamış mesajlar ilk + Okunmuş mesajlar ilk + Ekli mesajlar ilk + Eksiz mesajlar ilk + + Göre sırala... + Tarih + Gönderici + Konu + Yıldız + Okundu/Okunmadı + Ekler + %s + + Hesabı Kaldır + Hesap \"%s\" K-9 Mail\'den kaldırılacak. + + Hesabı canlandırmak + Bütün bilgi için \"%s\" K-9 Mail\'den kaldırılacak, fakat hesap ayarları korunacak. + + Hesabı Temizle + Bütün mesajlar içinde \"%s\" K-9 Mail\'den kaldırılacak, fakat hesap ayarları korunacak. + + Sadece bazı \"Plus\" hesaplar bu programın POP + erişiminden bağlanmasına izin verir.Eğer doğru eposta adresi ve şifre ile + oturum açamıyorsanız, \"Plus\" hesap için bir ödeme yapmamışsınız demektir. + Lütfen Web gezginini bu posta hesaplarına erişebilmek için başlatın. + Bu sağlayıcı için POP3 kullanmak isterseniz, Yahoo Mail ayarlar sayfasında POP3 kullanmak için vermelisiniz. + Bu sağlayıcı için IMAP veya POP3 kullanmak isterseniz, Naver mail ayarlar sayfasında IMAP veya POP3 kullanmaya izin vermelisiniz. + Bu sağlayıcı için IMAP veya POP3 kullanmak isterseniz, Hanmail (Daum) mail ayarlar sayfasında IMAP veya POP3 kullanmaya izin vermelisiniz . + Bu sağlayıcı için IMAP ve POP3 kullanmak isterseniz, Paran mail ayarlar sayfasında IMAP veya POP3 kullanmaya izin vermelisiniz. + Bu sağlayıcı için IMAP ve POP3 kullanmak isterseniz, Nate mail ayarlar sayfasında IMAP veya POP3 kullanmaya izin vermelisiniz. + + Tanınmayan Sertifika + Kabul Tuşu + Reddetme Tuşu + + Del (or D) - Sil\u000AR - + Cevapla\u000AA - Hepsini cevapla\u000AF - İlet\u000AJ or P - Önceki + Mesaj\u000AK, N - Sonraki Mesaj\u000AM - Taşı\u000AY - Kopyala\u000AZ - Uzaklaş\u000AShift-Z - + Yakınlaş\u000aG - Yıldız + Del (or D) - Sil\u000AR - + Cevapla\u000AA - Hepsini cevapla\u000AC - Oluştur\u000AF - İlet\u000aM - + Taşı\u000AY - Kopyala\u000AG - Yıldız\u000AO - Sıralama tipi\u000AI - Sıralama şekli\u000AQ + - Klasörlere dön\u000AS - Seç/seçme\u000AZ - Geçiş oku/okuma\u000A/ + - Ara\u000A5 - Postayı kontrol et + + + 1 - Yanlız 1nci Sınıf Klasörleri göster\u000A + 2 - Yanlız 1nci ve 2nci Sınıf klasörleri göster\u000A + 3 - 2nci Sınıf klasörler hariç hepsini göster\u000A + 4 - Bütün klasörleri göster\u000A + Q - Hesaplara dön\u000A + S - Hesap ayarlarını düzenle + + Klasörler + Bütün klasörleri göster + Sadece 1nci Sınıf klasörleri göster + 1nci ve 2nci Sınıf klasörleri göster + 2nci Sınıf klasörler hariç hepsini göster + + İmza Konumu + Saklanan metinden önce + Saklanan metinden sonra + Karanlık + Aydınlık + Görüntü + Genel + Hata ayıklama + Özel + + Etkileşim + Hesap Listesi + Mesaj Listeleri + Mesajlar + Tema + Dil + + Tek sütun düzeni + Daha küçük ekranlar için HTML mesajları yeniden biçimlendir + Sistem yakınlaştırma kontrolleri + Yakınlaştırma araçlarını veya cihazınız onu destekliyorsa pinch-zoom\'u etkinleştir + + Sistem Varsayılanları + + Arkaplan senkronizasyonu + Asla + Daima + \'Background data\' işaretli olduğu zaman + \'Background data\' & \'Auto-sync\' işaretli olduğu zaman + + Mesaj Seçilmedi + + Tarih Biçimi + + + Yığın Seçenekleri + Seçileni Sil + Seçilmiş olanı okundu işaretle + Seçilmiş olanı okunmadı işaretle + Seçilmiş olana Yıldız ekle + Seçilmi olandan Yıldızı kaldır + Seçilmişi Arşive Taşı + Seçilmişi Spam\'a Taşı + Seçileni Taşı + Seçileni Kopyala + Yıldız modu + Seçim modu + Düz mod + Hepsini Seç + Bütün seçimleri temizle + + push ile max klasörü kontrol et + 10 klasör + 25 klasör + 50 klasör + 100 klasör + 250 klasör + 500 klasör + 1000 klasör + + Animasyon + Gösterişli görsel efektleri kullan + Hareketleri + Hareket kontrolünü kabul et + + Kompakt düzen + Herbir sayfada daha fazlasını görüntülemek için düzeni ayarla + + Ses Düğmesi ile dolaşma + Ses kontrollerini kullanarak ürün sayfalarını çevir + Mesaj görünümü + Çeşitli liste görünümleri + + Yönet \"Geri\" düğmesi + Yap \"Geri\" daima bir üst seviyeye çıkart + + Birleşik Gelen Kutusu ile başla + Başlangıçta Birleşik Gelen Kutusunu göster + + Hesap boyutunu göster + Daha hızlı görüntüleme için kapatın + + Arama sayısı sonuçları + Daha hızlı görüntüleme için kapatın + + Özel hesapları sakla + Bütün mesaj hesaplarını ve birleşik gelen kutusunu sakla + + %s %s + - Yıldızlı + - Okunmadı + + Bütün mesajlar + Bütün mesajlar aranabilir klasörlerde + + Birleşik Gelen Kutusu + Bütün mesajlar birleşik gelen kutusunda + + Okunmamış veya yıldızlı mesajlar için yıldızı veya zarfı tıklayın + + Birleştirmek + Birleşik Gelen Kutusunda bütün mesajlar görünür + + Arama için Klasörler + Hepsi + Görüntülenebilir + Hiçbiri + + K-9 Posta\'ya uzaktan erişim + Bu programın K-9 Mail aktiviteleri ve ayarlarını kontrol edebilmesine izin ver. + + Yazı tipi Boyutu + Yazı tipi boyutunu yapılandır + + Hesap listesi + Hesap adı + Hesap tanımı + + Klasör listeleri + Klasör adı + Klasör durumu + + Mesaj listeleri + Konu + Gönderen + Tarih + Önizleme + + Mesajlar + Gönderen + Kime + Cc + Ek başlıklar + Konu + Zaman + Tarih + Mesaj gövdesi + + Çok küçük + Küçücük + Daha küçük + Küçük + Orta + Büyük + Daha büyük + + En küçük + Daha küçük + Normal + Daha büyük + En büyük + + + Kontrol et \"Ayarlar\" -> \"Use Gallery bug work-around\" 3D Galeri kullanarak imajlar veya videoları eklemeyi dene. + + + Kullan \"Ek Ekle (İmaj)\" veya \"Ek Ekle (Video)\" 3D Galeri ile imajları ve videoları eklemeyi. + + Çeşitli + Geçici çözüm galeri hatasını kullan + İmaj/Video ekleri ekleme butonlarını göster (Bir galeri 3D hatası geçici çözümüne) + + + Bu eylem için uygun program bulunamadı. + Kurulu APG versiyon desteklenmiyor. + İşaret + Şifrelemek + Çözmek + Doğrulamak + <unknown> + id: %s + K-9 tamamiyle APG erişimine izin vermez, lütfen bunu düzeltmek için K-9\'i yeniden kurun. + PGP/MIME mesajları henüz desteklenmiyor. + Uyarı: Ekler henüz işaretli veya şifreli değil. + İptal gönder. + + Taslak mesajı kaydet? + Bu mesajı kaydet veya at? + + Bu mesaj görüntülemez çünkü karakterler \"%s\" bulunamadı. + + Kopyalamak için metni seç. + + Silmeyi onayla + Bu mesajı silmek istiyormusunuz? + Sil + Silme + + Spam klasörüne taşımayı onayla + + Bu mesajı gerçekten Spam klasörüne taşımak istiyormusunuz? + Gerçekten taşımak istiyormusunuz %1$d mesajları Spam klasörüne? + + + Evet + Hayır + + Ek indiriliyor + + Hata günlüğü Android günlük sistemine etkinleştirildi. pay + + » + + Bağlanılamadı. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hesap \"%s\" mevcut değil; depolamayı kontrol et + + Ekleri kaydet... + Ekleri kaydet + Dosya gezgini bulunamadı. Bu eki nereye kaydetmek isterdiniz? + + + + + + diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index b7bc585e9..240c953dd 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -1030,6 +1030,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1038,5 +1079,6 @@ + diff --git a/res/values/arrays.xml b/res/values/arrays.xml index c6409ad84..7089a8471 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -504,6 +504,7 @@ pl pt_BR ru + tr zh_TW zh_CN fi diff --git a/res/values/strings.xml b/res/values/strings.xml index fe0bcd940..ee652f99a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1053,6 +1053,47 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Unable to connect. + Settings Import & Export + Export account settings + Export settings and accounts + Import + Export + Import settings + Import selection + Global settings + Exporting settings... + Importing settings... + Scanning file... + Saved exported settings to %s + Imported global settings from %s + Imported %s from %s + + 1 account + %s accounts + + Failed to export settings + Failed to import any settings from %s + Export succeeded + Export failed + Import succeeded + Import failed + Activate account + To be able to use the account \"%s\" you need to provide the %s. + + server password + server passwords + + Incoming server (%s): + Outgoing server (%s): + + Setting password... + Setting passwords... + + Use the incoming server password + Activate + + Unable to handle file of version %s + Account \"%s\" is unavailable; check storage Save attachments to... @@ -1061,5 +1102,6 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Move up Move down + Moving account... diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index f81fe794f..3a891c286 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -59,12 +59,21 @@ public class Account implements BaseAccount { public static final String TYPE_OTHER = "OTHER"; private static final String[] networkTypes = { TYPE_WIFI, TYPE_MOBILE, TYPE_OTHER }; - private static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML; - private static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false; - private static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX; - private static final String DEFAULT_QUOTE_PREFIX = ">"; - private static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true; - private static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; + public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML; + public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false; + public static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX; + public static final String DEFAULT_QUOTE_PREFIX = ">"; + public static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true; + public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; + + public static final String ACCOUNT_DESCRIPTION_KEY = "description"; + public static final String STORE_URI_KEY = "storeUri"; + public static final String TRANSPORT_URI_KEY = "transportUri"; + + public static final String IDENTITY_NAME_KEY = "name"; + public static final String IDENTITY_EMAIL_KEY = "email"; + public static final String IDENTITY_DESCRIPTION_KEY = "description"; + /** *
@@ -139,6 +148,16 @@ public class Account implements BaseAccount {
 
     private CryptoProvider mCryptoProvider = null;
 
+    /**
+     * Indicates whether this account is enabled, i.e. ready for use, or not.
+     *
+     * 

+ * Right now newly imported accounts are disabled if the settings file didn't contain a + * password for the incoming and/or outgoing server. + *

+ */ + private boolean mEnabled; + /** * Name of the folder that was last selected for a copy or move operation. * @@ -215,6 +234,7 @@ public class Account implements BaseAccount { mSyncRemoteDeletions = true; mCryptoApp = Apg.NAME; mCryptoAutoSignature = false; + mEnabled = true; searchableFolders = Searchable.ALL; @@ -377,8 +397,9 @@ public class Account implements BaseAccount { mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME); mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false); + mEnabled = prefs.getBoolean(mUuid + ".enabled", true); } - + protected synchronized void delete(Preferences preferences) { String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(","); String[] newUuids = new String[uuids.length - 1]; @@ -388,7 +409,7 @@ public class Account implements BaseAccount { newUuids[i++] = uuid; } } - + String accountUuids = Utility.combine(newUuids, ','); SharedPreferences.Editor editor = preferences.getPreferences().edit(); editor.putString("accountUuids", accountUuids); @@ -446,6 +467,7 @@ public class Account implements BaseAccount { editor.remove(mUuid + ".replyAfterQuote"); editor.remove(mUuid + ".cryptoApp"); editor.remove(mUuid + ".cryptoAutoSignature"); + editor.remove(mUuid + ".enabled"); editor.remove(mUuid + ".enableMoveButtons"); editor.remove(mUuid + ".hideMoveButtonsEnum"); for (String type : networkTypes) { @@ -480,7 +502,7 @@ public class Account implements BaseAccount { List accountNumbers = getExistingAccountNumbers(preferences); return findNewAccountNumber(accountNumbers); } - + public void move(Preferences preferences, boolean moveUp) { String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(","); SharedPreferences.Editor editor = preferences.getPreferences().edit(); @@ -489,7 +511,7 @@ public class Account implements BaseAccount { for (int i = 0; i < uuids.length; i++) { if (i > 0 && uuids[i].equals(mUuid)) { newUuids[i] = newUuids[i-1]; - newUuids[i-1] = mUuid; + newUuids[i-1] = mUuid; } else { newUuids[i] = uuids[i]; @@ -500,7 +522,7 @@ public class Account implements BaseAccount { for (int i = uuids.length - 1; i >= 0; i--) { if (i < uuids.length - 1 && uuids[i].equals(mUuid)) { newUuids[i] = newUuids[i+1]; - newUuids[i+1] = mUuid; + newUuids[i+1] = mUuid; } else { newUuids[i] = uuids[i]; @@ -510,9 +532,9 @@ public class Account implements BaseAccount { String accountUuids = Utility.combine(newUuids, ','); editor.putString("accountUuids", accountUuids); editor.commit(); - preferences.refreshAccounts(); + preferences.loadAccounts(); } - + public synchronized void save(Preferences preferences) { SharedPreferences.Editor editor = preferences.getPreferences().edit(); @@ -598,6 +620,7 @@ public class Account implements BaseAccount { editor.putBoolean(mUuid + ".replyAfterQuote", mReplyAfterQuote); editor.putString(mUuid + ".cryptoApp", mCryptoApp); editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature); + editor.putBoolean(mUuid + ".enabled", mEnabled); editor.putBoolean(mUuid + ".vibrate", mNotificationSetting.shouldVibrate()); editor.putInt(mUuid + ".vibratePattern", mNotificationSetting.getVibratePattern()); @@ -1096,18 +1119,17 @@ public class Account implements BaseAccount { return mUuid.hashCode(); } - private synchronized List loadIdentities(SharedPreferences prefs) { List newIdentities = new ArrayList(); int ident = 0; boolean gotOne = false; do { gotOne = false; - String name = prefs.getString(mUuid + ".name." + ident, null); - String email = prefs.getString(mUuid + ".email." + ident, null); + String name = prefs.getString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, null); + String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null); boolean signatureUse = prefs.getBoolean(mUuid + ".signatureUse." + ident, true); String signature = prefs.getString(mUuid + ".signature." + ident, null); - String description = prefs.getString(mUuid + ".description." + ident, null); + String description = prefs.getString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, null); final String replyTo = prefs.getString(mUuid + ".replyTo." + ident, null); if (email != null) { Identity identity = new Identity(); @@ -1145,13 +1167,13 @@ public class Account implements BaseAccount { boolean gotOne = false; do { gotOne = false; - String email = prefs.getString(mUuid + ".email." + ident, null); + String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null); if (email != null) { - editor.remove(mUuid + ".name." + ident); - editor.remove(mUuid + ".email." + ident); + editor.remove(mUuid + "." + IDENTITY_NAME_KEY + "." + ident); + editor.remove(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident); editor.remove(mUuid + ".signatureUse." + ident); editor.remove(mUuid + ".signature." + ident); - editor.remove(mUuid + ".description." + ident); + editor.remove(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident); editor.remove(mUuid + ".replyTo." + ident); gotOne = true; } @@ -1164,11 +1186,11 @@ public class Account implements BaseAccount { int ident = 0; for (Identity identity : identities) { - editor.putString(mUuid + ".name." + ident, identity.getName()); - editor.putString(mUuid + ".email." + ident, identity.getEmail()); + editor.putString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, identity.getName()); + editor.putString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, identity.getEmail()); editor.putBoolean(mUuid + ".signatureUse." + ident, identity.getSignatureUse()); editor.putString(mUuid + ".signature." + ident, identity.getSignature()); - editor.putString(mUuid + ".description." + ident, identity.getDescription()); + editor.putString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, identity.getDescription()); editor.putString(mUuid + ".replyTo." + ident, identity.getReplyTo()); ident++; } @@ -1460,4 +1482,11 @@ public class Account implements BaseAccount { return StorageManager.getInstance(K9.app).isReady(localStorageProviderId); } + public synchronized boolean isEnabled() { + return mEnabled; + } + + public synchronized void setEnabled(boolean enabled) { + mEnabled = enabled; + } } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index fd43cfc6a..6b8212295 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -463,59 +463,7 @@ public class K9 extends Application { galleryBuggy = checkForBuggyGallery(); Preferences prefs = Preferences.getPreferences(this); - SharedPreferences sprefs = prefs.getPreferences(); - DEBUG = sprefs.getBoolean("enableDebugLogging", false); - DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); - mAnimations = sprefs.getBoolean("animations", true); - mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true); - mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); - mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false); - mManageBack = sprefs.getBoolean("manageBack", false); - mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false); - mMeasureAccounts = sprefs.getBoolean("measureAccounts", true); - mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true); - mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false); - mMessageListStars = sprefs.getBoolean("messageListStars", true); - mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false); - mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false); - mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2); - - mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false); - mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false); - - mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false); - mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00"); - mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00"); - - mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true); - mShowContactName = sprefs.getBoolean("showContactName", false); - mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false); - mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f); - mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false); - mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false); - mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false); - - useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy()); - - mConfirmDelete = sprefs.getBoolean("confirmDelete", false); - mConfirmSpam = sprefs.getBoolean("confirmSpam", false); - mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true); - - - mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false); - - compactLayouts = sprefs.getBoolean("compactLayouts", false); - mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); - fontSizes.load(sprefs); - - try { - setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED"))); - } catch (Exception e) { - setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED); - } - - K9.setK9Language(sprefs.getString("language", "")); - K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); + loadPrefs(prefs); /* * We have to give MimeMessage a temp directory because File.createTempFile(String, String) @@ -587,6 +535,62 @@ public class K9 extends Application { notifyObservers(); } + public static void loadPrefs(Preferences prefs) { + SharedPreferences sprefs = prefs.getPreferences(); + DEBUG = sprefs.getBoolean("enableDebugLogging", false); + DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); + mAnimations = sprefs.getBoolean("animations", true); + mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true); + mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); + mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false); + mManageBack = sprefs.getBoolean("manageBack", false); + mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false); + mMeasureAccounts = sprefs.getBoolean("measureAccounts", true); + mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true); + mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false); + mMessageListStars = sprefs.getBoolean("messageListStars", true); + mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false); + mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false); + mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2); + + mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false); + mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false); + + mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false); + mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00"); + mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00"); + + mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true); + mShowContactName = sprefs.getBoolean("showContactName", false); + mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false); + mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f); + mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false); + mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false); + mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false); + + useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy()); + + mConfirmDelete = sprefs.getBoolean("confirmDelete", false); + mConfirmSpam = sprefs.getBoolean("confirmSpam", false); + mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true); + + + mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false); + + compactLayouts = sprefs.getBoolean("compactLayouts", false); + mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); + fontSizes.load(sprefs); + + try { + setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED"))); + } catch (Exception e) { + setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED); + } + + K9.setK9Language(sprefs.getString("language", "")); + K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); + } + private void maybeSetupStrictMode() { if (!K9.DEVELOPER_MODE) return; diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index f30c7e578..cbaa8623b 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -49,36 +49,23 @@ public class Preferences { } } - private synchronized void loadAccounts() { + public synchronized void loadAccounts() { accounts = new HashMap(); - refreshAccounts(); - } - - public synchronized void refreshAccounts() { - Map newAccountMap = new HashMap(); accountsInOrder = new LinkedList(); String accountUuids = getPreferences().getString("accountUuids", null); if ((accountUuids != null) && (accountUuids.length() != 0)) { String[] uuids = accountUuids.split(","); for (String uuid : uuids) { - Account account = accounts.get(uuid); - if (account != null) { - newAccountMap.put(uuid, account); - accountsInOrder.add(account); - } else { - Account newAccount = new Account(this, uuid); - newAccountMap.put(uuid, newAccount); - accountsInOrder.add(newAccount); - } + Account newAccount = new Account(this, uuid); + accounts.put(uuid, newAccount); + accountsInOrder.add(newAccount); } } if ((newAccount != null) && newAccount.getAccountNumber() != -1) { - newAccountMap.put(newAccount.getUuid(), newAccount); + accounts.put(newAccount.getUuid(), newAccount); accountsInOrder.add(newAccount); newAccount = null; } - - accounts = newAccountMap; } /** @@ -103,7 +90,7 @@ public class Preferences { Account[] allAccounts = getAccounts(); Collection retval = new ArrayList(accounts.size()); for (Account account : allAccounts) { - if (account.isAvailable(mContext)) { + if (account.isEnabled() && account.isAvailable(mContext)) { retval.add(account); } } @@ -129,12 +116,12 @@ public class Preferences { } public synchronized void deleteAccount(Account account) { - if (accounts != null) { - accounts.remove(account.getUuid()); - } - if (accountsInOrder != null) { - accountsInOrder.remove(account); - } + if (accounts != null) { + accounts.remove(account.getUuid()); + } + if (accountsInOrder != null) { + accountsInOrder.remove(account); + } account.delete(this); diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 4bc0d3291..c6ef7b397 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -1,6 +1,9 @@ package com.fsck.k9.activity; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; @@ -10,16 +13,24 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import android.app.Activity; import android.app.AlertDialog; +import android.app.Application; import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; +import android.util.SparseBooleanArray; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Menu; @@ -32,14 +43,22 @@ import android.view.View.OnClickListener; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CheckedTextView; +import android.widget.CompoundButton; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.CompoundButton.OnCheckedChangeListener; import com.fsck.k9.Account; import com.fsck.k9.AccountStats; @@ -50,6 +69,8 @@ import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.SearchAccount; import com.fsck.k9.SearchSpecification; +import com.fsck.k9.activity.misc.ExtendedAsyncTask; +import com.fsck.k9.activity.misc.NonConfigurationInstance; import com.fsck.k9.activity.setup.AccountSettings; import com.fsck.k9.activity.setup.AccountSetupBasics; import com.fsck.k9.activity.setup.Prefs; @@ -57,8 +78,21 @@ import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.SizeFormatter; import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.view.ColorChip; +import com.fsck.k9.preferences.SettingsExporter; +import com.fsck.k9.preferences.SettingsImportExportException; +import com.fsck.k9.preferences.SettingsImporter; +import com.fsck.k9.preferences.SettingsImporter.AccountDescription; +import com.fsck.k9.preferences.SettingsImporter.AccountDescriptionPair; +import com.fsck.k9.preferences.SettingsImporter.ImportContents; +import com.fsck.k9.preferences.SettingsImporter.ImportResults; + public class Accounts extends K9ListActivity implements OnItemClickListener, OnClickListener { @@ -88,6 +122,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC private SearchAccount integratedInboxAccount = null; private FontSizes mFontSizes = K9.getFontSizes(); + /** + * Contains information about objects that need to be retained on configuration changes. + * + * @see #onRetainNonConfigurationInstance() + */ + private NonConfigurationInstance mNonConfigurationInstance; + + + private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1; + class AccountsHandler extends Handler { private void setViewTitle() { String dispString = mListener.formatHeader(Accounts.this, getString(R.string.accounts_title), mUnreadMessageCount, getTimeFormat()); @@ -158,6 +202,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } + public void setProgress(boolean progress) { + mHandler.progress(progress); + } + ActivityListener mListener = new ActivityListener() { @Override public void informUserOfStatus() { @@ -256,6 +304,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC context.startActivity(intent); } + @Override + public void onNewIntent(Intent intent) { + Uri uri = intent.getData(); + Log.i(K9.LOG_TAG, "Accounts Activity got uri " + uri); + if (uri != null) { + ContentResolver contentResolver = getContentResolver(); + + Log.i(K9.LOG_TAG, "Accounts Activity got content of type " + contentResolver.getType(uri)); + + String contentType = contentResolver.getType(uri); + if (MimeUtility.K9_SETTINGS_MIME_TYPE.equals(contentType)) { + onImport(uri); + } + } + } @Override public void onCreate(Bundle icicle) { @@ -273,32 +336,41 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC Account[] accounts = Preferences.getPreferences(this).getAccounts(); Intent intent = getIntent(); + //onNewIntent(intent); + boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true); if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) { onOpenAccount(integratedInboxAccount); finish(); + return; } else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) { - // fall through to "else" if !onOpenAccount() finish(); - } else { - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - requestWindowFeature(Window.FEATURE_PROGRESS); + return; + } - setContentView(R.layout.accounts); - ListView listView = getListView(); - listView.setOnItemClickListener(this); - listView.setItemsCanFocus(false); - listView.setEmptyView(findViewById(R.id.empty)); - listView.setScrollingCacheEnabled(false); - findViewById(R.id.next).setOnClickListener(this); - registerForContextMenu(listView); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); - if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) { - String accountUuid = icicle.getString("selectedContextAccount"); - mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid); - } + setContentView(R.layout.accounts); + ListView listView = getListView(); + listView.setOnItemClickListener(this); + listView.setItemsCanFocus(false); + listView.setEmptyView(findViewById(R.id.empty)); + listView.setScrollingCacheEnabled(false); + findViewById(R.id.next).setOnClickListener(this); + registerForContextMenu(listView); - restoreAccountStats(icicle); + if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) { + String accountUuid = icicle.getString("selectedContextAccount"); + mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid); + } + + restoreAccountStats(icicle); + + // Handle activity restarts because of a configuration change (e.g. rotating the screen) + mNonConfigurationInstance = (NonConfigurationInstance) getLastNonConfigurationInstance(); + if (mNonConfigurationInstance != null) { + mNonConfigurationInstance.restore(this); } } @@ -348,9 +420,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC super.onPause(); MessagingController.getInstance(getApplication()).removeListener(mListener); StorageManager.getInstance(getApplication()).removeListener(storageListener); - } - + + /** + * Save the reference to a currently displayed dialog or a running AsyncTask (if available). + */ + @Override + public Object onRetainNonConfigurationInstance() { + Object retain = null; + if (mNonConfigurationInstance != null && mNonConfigurationInstance.retain()) { + retain = mNonConfigurationInstance; + } + return retain; + } + private BaseAccount[] accounts = new BaseAccount[0]; private enum ACCOUNT_LOCATION { TOP, MIDDLE, BOTTOM; @@ -365,12 +448,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC if (accounts[accounts.length - 1].equals(account)) { accountLocation.remove(ACCOUNT_LOCATION.MIDDLE); accountLocation.add(ACCOUNT_LOCATION.BOTTOM); - } + } } return accountLocation; } - - + + private void refresh() { accounts = Preferences.getPreferences(this).getAccounts(); @@ -472,7 +555,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount); } else { Account realAccount = (Account)account; - if (!realAccount.isAvailable(this)) { + if (!realAccount.isEnabled()) { + onActivateAccount(realAccount); + return false; + } else if (!realAccount.isAvailable(this)) { String toastText = getString(R.string.account_unavailable, account.getDescription()); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_SHORT); toast.show(); @@ -489,6 +575,319 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC return true; } + private void onActivateAccount(Account account) { + List disabledAccounts = new ArrayList(); + disabledAccounts.add(account); + promptForServerPasswords(disabledAccounts); + } + + /** + * Ask the user to enter the server passwords for disabled accounts. + * + * @param disabledAccounts + * A non-empty list of {@link Account}s to ask the user for passwords. Never + * {@code null}. + *

Note: Calling this method will modify the supplied list.

+ */ + private void promptForServerPasswords(final List disabledAccounts) { + Account account = disabledAccounts.remove(0); + PasswordPromptDialog dialog = new PasswordPromptDialog(account, disabledAccounts); + setNonConfigurationInstance(dialog); + dialog.show(this); + } + + /** + * Ask the user for the incoming/outgoing server passwords. + */ + private static class PasswordPromptDialog implements NonConfigurationInstance, TextWatcher { + private AlertDialog mDialog; + private EditText mIncomingPasswordView; + private EditText mOutgoingPasswordView; + private CheckBox mUseIncomingView; + + private Account mAccount; + private List mRemainingAccounts; + private String mIncomingPassword; + private String mOutgoingPassword; + private boolean mUseIncoming; + + /** + * Constructor + * + * @param account + * The {@link Account} to ask the server passwords for. Never {@code null}. + * @param accounts + * The (possibly empty) list of remaining accounts to ask passwords for. Never + * {@code null}. + */ + PasswordPromptDialog(Account account, List accounts) { + mAccount = account; + mRemainingAccounts = accounts; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity, true); + } + + @Override + public boolean retain() { + if (mDialog != null) { + // Retain entered passwords and checkbox state + mIncomingPassword = mIncomingPasswordView.getText().toString(); + if (mOutgoingPasswordView != null) { + mOutgoingPassword = mOutgoingPasswordView.getText().toString(); + mUseIncoming = mUseIncomingView.isChecked(); + } + + // Dismiss dialog + mDialog.dismiss(); + + // Clear all references to UI objects + mDialog = null; + mIncomingPasswordView = null; + mOutgoingPasswordView = null; + mUseIncomingView = null; + return true; + } + return false; + } + + public void show(Accounts activity) { + show(activity, false); + } + + private void show(final Accounts activity, boolean restore) { + ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri()); + ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri()); + + // Don't ask for the password to the outgoing server for WebDAV accounts, because + // incoming and outgoing servers are identical for this account type. + boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type); + + // Create a ScrollView that will be used as container for the whole layout + final ScrollView scrollView = new ScrollView(activity); + + // Create the dialog + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.settings_import_activate_account_header)); + builder.setView(scrollView); + builder.setPositiveButton(activity.getString(R.string.okay_action), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String incomingPassword = mIncomingPasswordView.getText().toString(); + String outgoingPassword = null; + if (mOutgoingPasswordView != null) { + outgoingPassword = (mUseIncomingView.isChecked()) ? + incomingPassword : mOutgoingPasswordView.getText().toString(); + } + + dialog.dismiss(); + + // Set the server passwords in the background + SetPasswordsAsyncTask asyncTask = new SetPasswordsAsyncTask(activity, mAccount, + incomingPassword, outgoingPassword, mRemainingAccounts); + activity.setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + }); + builder.setNegativeButton(activity.getString(R.string.cancel_action), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + } + }); + mDialog = builder.create(); + + // Use the dialog's layout inflater so its theme is used (and not the activity's theme). + View layout = mDialog.getLayoutInflater().inflate( + R.layout.accounts_password_prompt, null); + + // Set the intro text that tells the user what to do + TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro); + String serverPasswords = activity.getResources().getQuantityString( + R.plurals.settings_import_server_passwords, + (configureOutgoingServer) ? 2 : 1); + intro.setText(activity.getString(R.string.settings_import_activate_account_intro, + mAccount.getDescription(), serverPasswords)); + + // Display the hostname of the incoming server + TextView incomingText = (TextView) layout.findViewById( + R.id.password_prompt_incoming_server); + incomingText.setText(activity.getString(R.string.settings_import_incoming_server, + incoming.host)); + + mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password); + mIncomingPasswordView.addTextChangedListener(this); + + if (configureOutgoingServer) { + // Display the hostname of the outgoing server + TextView outgoingText = (TextView) layout.findViewById( + R.id.password_prompt_outgoing_server); + outgoingText.setText(activity.getString(R.string.settings_import_outgoing_server, + outgoing.host)); + + mOutgoingPasswordView = (EditText) layout.findViewById( + R.id.outgoing_server_password); + mOutgoingPasswordView.addTextChangedListener(this); + + mUseIncomingView = (CheckBox) layout.findViewById( + R.id.use_incoming_server_password); + mUseIncomingView.setChecked(true); + mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mOutgoingPasswordView.setText(null); + mOutgoingPasswordView.setEnabled(false); + } else { + mOutgoingPasswordView.setText(mIncomingPasswordView.getText()); + mOutgoingPasswordView.setEnabled(true); + } + } + }); + } else { + layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE); + } + + // Add the layout to the ScrollView + scrollView.addView(layout); + + // Show the dialog + mDialog.show(); + + // Restore the contents of the password boxes and the checkbox (if the dialog was + // retained during a configuration change). + if (restore) { + mIncomingPasswordView.setText(mIncomingPassword); + if (configureOutgoingServer) { + mOutgoingPasswordView.setText(mOutgoingPassword); + mUseIncomingView.setChecked(mUseIncoming); + } + } else { + // Trigger afterTextChanged() being called + // Work around this bug: https://code.google.com/p/android/issues/detail?id=6360 + mIncomingPasswordView.setText(mIncomingPasswordView.getText()); + } + } + + @Override + public void afterTextChanged(Editable arg0) { + boolean enable = false; + // Is the password box for the incoming server password empty? + if (mIncomingPasswordView.getText().length() > 0) { + // Do we need to check the outgoing server password box? + if (mOutgoingPasswordView == null) { + enable = true; + } + // If the checkbox to use the incoming server password is checked we need to make + // sure that the password box for the outgoing server isn't empty. + else if (mUseIncomingView.isChecked() || + mOutgoingPasswordView.getText().length() > 0) { + enable = true; + } + } + + // Disable "OK" button if the user hasn't specified all necessary passwords. + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Not used + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Not used + } + } + + /** + * Set the incoming/outgoing server password in the background. + */ + private static class SetPasswordsAsyncTask extends ExtendedAsyncTask { + private Account mAccount; + private String mIncomingPassword; + private String mOutgoingPassword; + private List mRemainingAccounts; + private Application mApplication; + + protected SetPasswordsAsyncTask(Activity activity, Account account, + String incomingPassword, String outgoingPassword, + List remainingAccounts) { + super(activity); + mAccount = account; + mIncomingPassword = incomingPassword; + mOutgoingPassword = outgoingPassword; + mRemainingAccounts = remainingAccounts; + mApplication = mActivity.getApplication(); + } + + @Override + protected void showProgressDialog() { + String title = mActivity.getString(R.string.settings_import_activate_account_header); + int passwordCount = (mOutgoingPassword == null) ? 1 : 2; + String message = mActivity.getResources().getQuantityString( + R.plurals.settings_import_setting_passwords, passwordCount); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Void doInBackground(Void... params) { + try { + // Set incoming server password + String storeUri = mAccount.getStoreUri(); + ServerSettings incoming = Store.decodeStoreUri(storeUri); + ServerSettings newIncoming = incoming.newPassword(mIncomingPassword); + String newStoreUri = Store.createStoreUri(newIncoming); + mAccount.setStoreUri(newStoreUri); + + if (mOutgoingPassword != null) { + // Set outgoing server password + String transportUri = mAccount.getTransportUri(); + ServerSettings outgoing = Transport.decodeTransportUri(transportUri); + ServerSettings newOutgoing = outgoing.newPassword(mOutgoingPassword); + String newTransportUri = Transport.createTransportUri(newOutgoing); + mAccount.setTransportUri(newTransportUri); + } + + // Mark account as enabled + mAccount.setEnabled(true); + + // Save the account settings + mAccount.save(Preferences.getPreferences(mContext)); + + // Start services if necessary + K9.setServicesEnabled(mContext); + + // Get list of folders from remote server + MessagingController.getInstance(mApplication).listFolders(mAccount, true, null); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Something went while setting account passwords", e); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + activity.refresh(); + removeProgressDialog(); + + if (mRemainingAccounts.size() > 0) { + activity.promptForServerPasswords(mRemainingAccounts); + } + } + } + public void onClick(View view) { if (view.getId() == R.id.next) { onAddNewAccount(); @@ -613,6 +1012,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.open: onOpenAccount(mSelectedContextAccount); break; + case R.id.activate: + onActivateAccount(realAccount); + break; case R.id.check_mail: onCheckMail(realAccount); break; @@ -631,6 +1033,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.recreate: onRecreate(realAccount); break; + case R.id.export: + onExport(false, realAccount); + break; case R.id.move_up: onMove(realAccount, true); break; @@ -656,22 +1061,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC showDialog(DIALOG_RECREATE_ACCOUNT); } private void onMove(final Account account, final boolean up) { - mHandler.progress(true); - AsyncUIProcessor.getInstance(getApplication()).execute( - new Runnable() - { - @Override - public void run() { - account.move(Preferences.getPreferences(Accounts.this), up); - runOnUiThread(new Runnable() { - @Override - public void run() { - refresh(); - mHandler.progress(false); - } - }); - } - }); + MoveAccountAsyncTask asyncTask = new MoveAccountAsyncTask(this, account, up); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); } public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -700,6 +1092,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.search: onSearchRequested(); break; + case R.id.export_all: + onExport(true, null); + break; + case R.id.import_settings: + onImport(); + break; default: return super.onOptionsItemSelected(item); } @@ -800,10 +1198,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.setHeaderTitle(R.string.accounts_context_menu_title); - getMenuInflater().inflate(R.menu.accounts_context, menu); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; BaseAccount account = mAdapter.getItem(info.position); + + if ((account instanceof Account) && !((Account) account).isEnabled()) { + getMenuInflater().inflate(R.menu.disabled_accounts_context, menu); + } else { + getMenuInflater().inflate(R.menu.accounts_context, menu); + } + if (account instanceof SearchAccount) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); @@ -829,6 +1233,321 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } + private void onImport() { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType(MimeUtility.K9_SETTINGS_MIME_TYPE); + startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.i(K9.LOG_TAG, "onActivityResult requestCode = " + requestCode + ", resultCode = " + resultCode + ", data = " + data); + if (resultCode != RESULT_OK) + return; + if (data == null) { + return; + } + switch (requestCode) { + case ACTIVITY_REQUEST_PICK_SETTINGS_FILE: + onImport(data.getData()); + break; + } + } + + private void onImport(Uri uri) { + ListImportContentsAsyncTask asyncTask = new ListImportContentsAsyncTask(this, uri); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + + + private void showSimpleDialog(int headerRes, int messageRes, Object... args) { + SimpleDialog dialog = new SimpleDialog(headerRes, messageRes, args); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A simple dialog. + */ + private static class SimpleDialog implements NonConfigurationInstance { + private final int mHeaderRes; + private final int mMessageRes; + private Object[] mArguments; + private Dialog mDialog; + + SimpleDialog(int headerRes, int messageRes, Object... args) { + this.mHeaderRes = headerRes; + this.mMessageRes = messageRes; + this.mArguments = args; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity); + } + + @Override + public boolean retain() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + return true; + } + return false; + } + + public void show(final Accounts activity) { + final String message = generateMessage(activity); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(mHeaderRes); + builder.setMessage(message); + builder.setPositiveButton(R.string.okay_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + okayAction(activity); + } + }); + mDialog = builder.show(); + } + + /** + * Returns the message the dialog should display. + * + * @param activity + * The {@code Activity} this dialog belongs to. + * + * @return The message the dialog should display + */ + protected String generateMessage(Accounts activity) { + return activity.getString(mMessageRes, mArguments); + } + + /** + * This method is called after the "OK" button was pressed. + * + * @param activity + * The {@code Activity} this dialog belongs to. + */ + protected void okayAction(Accounts activity) { + // Do nothing + } + } + + /** + * Shows a dialog that displays how many accounts were successfully imported. + * + * @param importResults + * The {@link ImportResults} instance returned by the {@link SettingsImporter}. + * @param filename + * The name of the settings file that was imported. + */ + private void showAccountsImportedDialog(ImportResults importResults, String filename) { + AccountsImportedDialog dialog = new AccountsImportedDialog(importResults, filename); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A dialog that displays how many accounts were successfully imported. + */ + private static class AccountsImportedDialog extends SimpleDialog { + private ImportResults mImportResults; + private String mFilename; + + AccountsImportedDialog(ImportResults importResults, String filename) { + super(R.string.settings_import_success_header, R.string.settings_import_success); + mImportResults = importResults; + mFilename = filename; + } + + @Override + protected String generateMessage(Accounts activity) { + //TODO: display names of imported accounts (name from file *and* possibly new name) + + int imported = mImportResults.importedAccounts.size(); + String accounts = activity.getResources().getQuantityString( + R.plurals.settings_import_success, imported, imported); + return activity.getString(R.string.settings_import_success, accounts, mFilename); + } + + @Override + protected void okayAction(Accounts activity) { + Context context = activity.getApplicationContext(); + Preferences preferences = Preferences.getPreferences(context); + List disabledAccounts = new ArrayList(); + for (AccountDescriptionPair accountPair : mImportResults.importedAccounts) { + Account account = preferences.getAccount(accountPair.imported.uuid); + if (account != null && !account.isEnabled()) { + disabledAccounts.add(account); + } + } + if (disabledAccounts.size() > 0) { + activity.promptForServerPasswords(disabledAccounts); + } else { + activity.setNonConfigurationInstance(null); + } + } + } + + /** + * Display a dialog that lets the user select which accounts to import from the settings file. + * + * @param importContents + * The {@link ImportContents} instance returned by + * {@link SettingsImporter#getImportStreamContents(InputStream)} + * @param uri + * The (content) URI of the settings file. + */ + private void showImportSelectionDialog(ImportContents importContents, Uri uri) { + ImportSelectionDialog dialog = new ImportSelectionDialog(importContents, uri); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A dialog that lets the user select which accounts to import from the settings file. + */ + private static class ImportSelectionDialog implements NonConfigurationInstance { + private ImportContents mImportContents; + private Uri mUri; + private Dialog mDialog; + private ListView mImportSelectionView; + private SparseBooleanArray mSelection; + + + ImportSelectionDialog(ImportContents importContents, Uri uri) { + mImportContents = importContents; + mUri = uri; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity, mSelection); + } + + @Override + public boolean retain() { + if (mDialog != null) { + // Save the selection state of each list item + mSelection = mImportSelectionView.getCheckedItemPositions(); + mImportSelectionView = null; + + mDialog.dismiss(); + mDialog = null; + return true; + } + return false; + } + + public void show(Accounts activity) { + show(activity, null); + } + + public void show(final Accounts activity, SparseBooleanArray selection) { + final ListView importSelectionView = new ListView(activity); + mImportSelectionView = importSelectionView; + List contents = new ArrayList(); + + if (mImportContents.globalSettings) { + contents.add(activity.getString(R.string.settings_import_global_settings)); + } + + for (AccountDescription account : mImportContents.accounts) { + contents.add(account.name); + } + + importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + importSelectionView.setAdapter(new ArrayAdapter(activity, + android.R.layout.simple_list_item_checked, contents)); + importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + CheckedTextView ctv = (CheckedTextView)view; + ctv.setChecked(!ctv.isChecked()); + } + + @Override + public void onNothingSelected(AdapterView arg0) { /* Do nothing */ } + }); + + if (selection != null) { + for (int i = 0, end = contents.size(); i < end; i++) { + importSelectionView.setItemChecked(i, selection.get(i)); + } + } + + //TODO: listview header: "Please select the settings you wish to import" + //TODO: listview footer: "Select all" / "Select none" buttons? + //TODO: listview footer: "Overwrite existing accounts?" checkbox + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.settings_import_selection)); + builder.setView(importSelectionView); + builder.setInverseBackgroundForced(true); + builder.setPositiveButton(R.string.okay_action, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ListAdapter adapter = importSelectionView.getAdapter(); + int count = adapter.getCount(); + SparseBooleanArray pos = importSelectionView.getCheckedItemPositions(); + + boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false; + List accountUuids = new ArrayList(); + int start = mImportContents.globalSettings ? 1 : 0; + for (int i = start; i < count; i++) { + if (pos.get(i)) { + accountUuids.add(mImportContents.accounts.get(i-start).uuid); + } + } + + /* + * TODO: Think some more about this. Overwriting could change the store + * type. This requires some additional code in order to work smoothly + * while the app is running. + */ + boolean overwrite = false; + + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + + ImportAsyncTask importAsyncTask = new ImportAsyncTask(activity, + includeGlobals, accountUuids, overwrite, mUri); + activity.setNonConfigurationInstance(importAsyncTask); + importAsyncTask.execute(); + } + }); + builder.setNegativeButton(R.string.cancel_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + } + }); + mDialog = builder.show(); + } + } + + /** + * Set the {@code NonConfigurationInstance} this activity should retain on configuration + * changes. + * + * @param inst + * The {@link NonConfigurationInstance} that should be retained when + * {@link Accounts#onRetainNonConfigurationInstance()} is called. + */ + private void setNonConfigurationInstance(NonConfigurationInstance inst) { + mNonConfigurationInstance = inst; + } + class AccountsAdapter extends ArrayAdapter { public AccountsAdapter(BaseAccount[] accounts) { super(Accounts.this, 0, accounts); @@ -1026,4 +1745,246 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } + public void onExport(final boolean includeGlobals, final Account account) { + + // TODO, prompt to allow a user to choose which accounts to export + Set accountUuids = null; + if (account != null) { + accountUuids = new HashSet(); + accountUuids.add(account.getUuid()); + } + + ExportAsyncTask asyncTask = new ExportAsyncTask(this, includeGlobals, accountUuids); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + + /** + * Handles exporting of global settings and/or accounts in a background thread. + */ + private static class ExportAsyncTask extends ExtendedAsyncTask { + private boolean mIncludeGlobals; + private Set mAccountUuids; + private String mFileName; + + + private ExportAsyncTask(Accounts activity, boolean includeGlobals, + Set accountUuids) { + super(activity); + mIncludeGlobals = includeGlobals; + mAccountUuids = accountUuids; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_export_dialog_title); + String message = mContext.getString(R.string.settings_exporting); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + mFileName = SettingsExporter.exportToFile(mContext, mIncludeGlobals, + mAccountUuids); + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + if (success) { + activity.showSimpleDialog(R.string.settings_export_success_header, + R.string.settings_export_success, mFileName); + } else { + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_export_failed_header, + R.string.settings_export_failure); + } + } + } + + /** + * Handles importing of global settings and/or accounts in a background thread. + */ + private static class ImportAsyncTask extends ExtendedAsyncTask { + private boolean mIncludeGlobals; + private List mAccountUuids; + private boolean mOverwrite; + private Uri mUri; + private ImportResults mImportResults; + + private ImportAsyncTask(Accounts activity, boolean includeGlobals, + List accountUuids, boolean overwrite, Uri uri) { + super(activity); + mIncludeGlobals = includeGlobals; + mAccountUuids = accountUuids; + mOverwrite = overwrite; + mUri = uri; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_import_dialog_title); + String message = mContext.getString(R.string.settings_importing); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + InputStream is = mContext.getContentResolver().openInputStream(mUri); + try { + mImportResults = SettingsImporter.importSettings(mContext, is, + mIncludeGlobals, mAccountUuids, mOverwrite); + } finally { + try { + is.close(); + } catch (IOException e) { /* Ignore */ } + } + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during import", e); + return false; + } catch (FileNotFoundException e) { + Log.w(K9.LOG_TAG, "Couldn't open import file", e); + return false; + } catch (Exception e) { + Log.w(K9.LOG_TAG, "Unknown error", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + String filename = mUri.getLastPathSegment(); + boolean globalSettings = mImportResults.globalSettings; + int imported = mImportResults.importedAccounts.size(); + if (success && (globalSettings || imported > 0)) { + if (imported == 0) { + activity.showSimpleDialog(R.string.settings_import_success_header, + R.string.settings_import_global_settings_success, filename); + } else { + activity.showAccountsImportedDialog(mImportResults, filename); + } + + activity.refresh(); + } else { + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_import_failed_header, + R.string.settings_import_failure, filename); + } + } + } + + private static class ListImportContentsAsyncTask extends ExtendedAsyncTask { + private Uri mUri; + private ImportContents mImportContents; + + private ListImportContentsAsyncTask(Accounts activity, Uri uri) { + super(activity); + + mUri = uri; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_import_dialog_title); + String message = mContext.getString(R.string.settings_import_scanning_file); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + ContentResolver resolver = mContext.getContentResolver(); + InputStream is = resolver.openInputStream(mUri); + try { + mImportContents = SettingsImporter.getImportStreamContents(is); + } finally { + try { + is.close(); + } catch (IOException e) { /* Ignore */ } + } + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + catch (FileNotFoundException e) { + Log.w(K9.LOG_TAG, "Couldn't read content from URI " + mUri); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + if (success) { + activity.showImportSelectionDialog(mImportContents, mUri); + } else { + String filename = mUri.getLastPathSegment(); + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_import_failed_header, + R.string.settings_import_failure, filename); + } + } + } + + private static class MoveAccountAsyncTask extends ExtendedAsyncTask { + private Account mAccount; + private boolean mUp; + + protected MoveAccountAsyncTask(Activity activity, Account account, boolean up) { + super(activity); + mAccount = account; + mUp = up; + } + + @Override + protected void showProgressDialog() { + String message = mActivity.getString(R.string.manage_accounts_moving_message); + mProgressDialog = ProgressDialog.show(mActivity, null, message, true); + } + + @Override + protected Void doInBackground(Void... args) { + mAccount.move(Preferences.getPreferences(mContext), mUp); + return null; + } + + @Override + protected void onPostExecute(Void arg) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + activity.refresh(); + removeProgressDialog(); + } + } } diff --git a/src/com/fsck/k9/activity/AsyncUIProcessor.java b/src/com/fsck/k9/activity/AsyncUIProcessor.java deleted file mode 100644 index 55947b2c4..000000000 --- a/src/com/fsck/k9/activity/AsyncUIProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.fsck.k9.activity; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import android.app.Application; - -/** - * The class should be used to run long-running processes invoked from the UI that - * do not affect the Stores. There are probably pieces of MessagingController - * that can be moved here. There is no wakelock used here. Any network activity, or - * true background activity, that is invoked from here should wakelock itself. UI-centric - * activity does not need to be wakelocked, as it will simply continue when the phone wakes - * without disruption. - * - */ -public class AsyncUIProcessor { - - private final ExecutorService threadPool = Executors.newCachedThreadPool(); - private static AsyncUIProcessor inst = null; - private AsyncUIProcessor() { - } - - public synchronized static AsyncUIProcessor getInstance(Application application) { - if (inst == null) { - inst = new AsyncUIProcessor(); - } - return inst; - } - public void execute(Runnable runnable) { - threadPool.execute(runnable); - } -} diff --git a/src/com/fsck/k9/activity/ChooseAccount.java b/src/com/fsck/k9/activity/ChooseAccount.java index 6b04f6b76..0c1c0041c 100644 --- a/src/com/fsck/k9/activity/ChooseAccount.java +++ b/src/com/fsck/k9/activity/ChooseAccount.java @@ -28,6 +28,7 @@ import java.util.List; * @see K9ExpandableListActivity */ public class ChooseAccount extends K9ExpandableListActivity { + private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[0]; /** * {@link Intent} extended data name for storing {@link Account#getUuid() @@ -50,7 +51,7 @@ public class ChooseAccount extends K9ExpandableListActivity { final ExpandableListView expandableListView = getExpandableListView(); expandableListView.setItemsCanFocus(false); - final ExpandableListAdapter adapter = createAdapter(); + final IdentitiesAdapter adapter = createAdapter(); setListAdapter(adapter); expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @@ -77,7 +78,7 @@ public class ChooseAccount extends K9ExpandableListActivity { final Bundle extras = getIntent().getExtras(); final String uuid = extras.getString(EXTRA_ACCOUNT); if (uuid != null) { - final Account[] accounts = Preferences.getPreferences(this).getAccounts(); + final Account[] accounts = adapter.getAccounts(); final int length = accounts.length; for (int i = 0; i < length; i++) { final Account account = accounts[i]; @@ -106,7 +107,7 @@ public class ChooseAccount extends K9ExpandableListActivity { } } - private ExpandableListAdapter createAdapter() { + private IdentitiesAdapter createAdapter() { return new IdentitiesAdapter(this, getLayoutInflater()); } @@ -123,10 +124,13 @@ public class ChooseAccount extends K9ExpandableListActivity { private Context mContext; private LayoutInflater mLayoutInflater; + private Account[] mAccounts; public IdentitiesAdapter(final Context context, final LayoutInflater layoutInflater) { mContext = context; mLayoutInflater = layoutInflater; + Preferences prefs = Preferences.getPreferences(mContext); + mAccounts = prefs.getAvailableAccounts().toArray(EMPTY_ACCOUNT_ARRAY); } @Override @@ -233,7 +237,7 @@ public class ChooseAccount extends K9ExpandableListActivity { } private Account[] getAccounts() { - return Preferences.getPreferences(mContext).getAccounts(); + return mAccounts; } } } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index c161e23e5..47b346fcc 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -152,6 +152,7 @@ public class FolderList extends K9ListActivity { } } + /** * This class is responsible for reloading the list of local messages for a * given folder, notifying the adapter that the message have been loaded and diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java index b2c1827c6..263324671 100644 --- a/src/com/fsck/k9/activity/K9Activity.java +++ b/src/com/fsck/k9/activity/K9Activity.java @@ -195,6 +195,4 @@ public class K9Activity extends Activity { return false; } } - - } diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index c14486d37..3a282a947 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -2183,7 +2183,9 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc if (quotedHTML.length() > 0) { mQuotedHtmlContent = new InsertableHtmlContent(); mQuotedHtmlContent.setQuotedContent(quotedHTML); + // We don't know if bodyOffset refers to the header or to the footer mQuotedHtmlContent.setHeaderInsertionPoint(bodyOffset); + mQuotedHtmlContent.setFooterInsertionPoint(bodyOffset); mQuotedHTML.loadDataWithBaseURL("http://", mQuotedHtmlContent.getQuotedContent(), "text/html", "utf-8", null); } } diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 626f522fb..7cb10161f 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -25,7 +25,6 @@ import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; -import android.view.GestureDetector.SimpleOnGestureListener; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -554,6 +553,9 @@ public class MessageList public static Intent actionHandleFolderIntent(Context context, Account account, String folder) { Intent intent = new Intent(context, MessageList.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(EXTRA_ACCOUNT, account.getUuid()); if (folder != null) { @@ -573,6 +575,9 @@ public class MessageList } intent.putExtra(EXTRA_INTEGRATE, integrate); intent.putExtra(EXTRA_TITLE, title); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } @@ -589,6 +594,9 @@ public class MessageList intent.putExtra(EXTRA_ACCOUNT_UUIDS, searchSpecification.getAccountUuids()); intent.putExtra(EXTRA_FOLDER_NAMES, searchSpecification.getFolderNames()); intent.putExtra(EXTRA_TITLE, title); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); } @@ -618,6 +626,12 @@ public class MessageList mInflater = getLayoutInflater(); initializeLayout(); + + // Only set "touchable" when we're first starting up the activity. + // Otherwise we get force closes when the user toggles it midstream. + mTouchView = K9.messageListTouchable(); + mPreviewLines = K9.messageListPreviewLines(); + onNewIntent(getIntent()); } @@ -625,22 +639,30 @@ public class MessageList public void onNewIntent(Intent intent) { setIntent(intent); // onNewIntent doesn't autoset our "internal" intent - // Only set "touchable" when we're first starting up the activity. - // Otherwise we get force closes when the user toggles it midstream. - mTouchView = K9.messageListTouchable(); - mPreviewLines = K9.messageListPreviewLines(); - String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - mFolderName = intent.getStringExtra(EXTRA_FOLDER); - mQueryString = intent.getStringExtra(EXTRA_QUERY); + Account account = Preferences.getPreferences(this).getAccount(accountUuid); + String folderName = intent.getStringExtra(EXTRA_FOLDER); + String queryString = intent.getStringExtra(EXTRA_QUERY); - if (mAccount != null && !mAccount.isAvailable(this)) { + if (account != null && !account.isAvailable(this)) { Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account"); onAccountUnavailable(); return; } + if (account != null && account.equals(mAccount) && + folderName != null && folderName.equals(mFolderName) && + ((queryString != null && queryString.equals(mQueryString)) || + (queryString == null && mQueryString == null))) { + // We're likely just returning from the MessageView activity with "Manage back button" + // enabled. So just leave the activity in the state it was left in. + return; + } + + mAccount = account; + mFolderName = folderName; + mQueryString = queryString; + String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS); if (queryFlags != null) { String[] flagStrings = queryFlags.split(","); @@ -1070,7 +1092,7 @@ public class MessageList MessageReference ref = message.message.makeMessageReference(); Log.i(K9.LOG_TAG, "MessageList sending message " + ref); - MessageView.actionView(this, ref, messageRefs, getIntent()); + MessageView.actionView(this, ref, messageRefs); } /* diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index 66670ab3a..f969b9d55 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -1,7 +1,5 @@ package com.fsck.k9.activity; -import android.app.ActivityManager; -import android.app.ActivityManager.RunningTaskInfo; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; @@ -36,8 +34,8 @@ import java.util.*; public class MessageView extends K9Activity implements OnClickListener { private static final String EXTRA_MESSAGE_REFERENCE = "com.fsck.k9.MessageView_messageReference"; private static final String EXTRA_MESSAGE_REFERENCES = "com.fsck.k9.MessageView_messageReferences"; - private static final String EXTRA_ORIGINATING_INTENT = "com.fsck.k9.MessageView_originatingIntent"; private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next"; + private static final String EXTRA_SCROLL_PERCENTAGE = "com.fsck.k9.MessageView_scrollPercentage"; private static final String SHOW_PICTURES = "showPictures"; private static final String STATE_PGP_DATA = "pgpData"; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; @@ -65,12 +63,6 @@ public class MessageView extends K9Activity implements OnClickListener { HAS_SUPER_ON_BACK_METHOD = hasOnBackMethod; } - /** - * If user opt-in for the "Manage BACK button", we have to remember how to get back to the - * originating activity (just recreating a new Intent could lose the calling activity state) - */ - private Intent mCreatorIntent; - private SingleMessageView mMessageView; private PgpData mPgpData; @@ -275,20 +267,8 @@ public class MessageView extends K9Activity implements OnClickListener { // or later, or by the code above on earlier versions of the // platform. if (K9.manageBack()) { - final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - // retrieve the current+previous tasks - final List runningTasks = activityManager.getRunningTasks(2); - final RunningTaskInfo previousTask = runningTasks.get(1); - final String originatingActivity = mCreatorIntent.getComponent().getClassName(); - if (originatingActivity.equals(previousTask.topActivity.getClassName())) { - // we can safely just finish ourself since the most recent task matches our creator - // this enable us not to worry about restoring the state of our creator - } else { - // the previous task top activity doesn't match our creator (previous task is from - // another app and user used long-pressed-HOME to display MessageView) - // launching our creator - startActivity(mCreatorIntent); - } + String folder = (mMessage != null) ? mMessage.getFolder().getName() : null; + MessageList.actionHandleFolder(this, mAccount, folder); finish(); } else if (HAS_SUPER_ON_BACK_METHOD) { super.onBackPressed(); @@ -348,29 +328,15 @@ public class MessageView extends K9Activity implements OnClickListener { } - /** - * @param context - * @param messRef - * @param messReferences - * @param originatingIntent - * The intent that allow us to get back to the calling screen, for when the 'Manage - * "Back" button' option is enabled. Never {@code null}. - */ public static void actionView(Context context, MessageReference messRef, - ArrayList messReferences, final Intent originatingIntent) { + ArrayList messReferences) { Intent i = new Intent(context, MessageView.class); i.putExtra(EXTRA_MESSAGE_REFERENCE, messRef); i.putParcelableArrayListExtra(EXTRA_MESSAGE_REFERENCES, messReferences); - i.putExtra(EXTRA_ORIGINATING_INTENT, originatingIntent); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(i); } - @Override - protected void onNewIntent(final Intent intent) { - mCreatorIntent = intent.getParcelableExtra(EXTRA_ORIGINATING_INTENT); - } - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle, false); @@ -407,15 +373,19 @@ public class MessageView extends K9Activity implements OnClickListener { } }; }); + mMessageView.initialize(this); + // Register the ScrollView's listener to handle scrolling to last known location on resume. + mController.addListener(mTopView.getListener()); + mMessageView.setListeners(mController.getListeners()); + setTitle(""); final Intent intent = getIntent(); - mCreatorIntent = getIntent().getParcelableExtra(EXTRA_ORIGINATING_INTENT); - Uri uri = intent.getData(); if (icicle != null) { + // TODO This code seems unnecessary since the icicle should already be thawed in onRestoreInstanceState(). mMessageReference = icicle.getParcelable(EXTRA_MESSAGE_REFERENCE); mMessageReferences = icicle.getParcelableArrayList(EXTRA_MESSAGE_REFERENCES); mPgpData = (PgpData) icicle.getSerializable(STATE_PGP_DATA); @@ -529,6 +499,7 @@ public class MessageView extends K9Activity implements OnClickListener { outState.putParcelableArrayList(EXTRA_MESSAGE_REFERENCES, mMessageReferences); outState.putSerializable(STATE_PGP_DATA, mPgpData); outState.putBoolean(SHOW_PICTURES, mMessageView.showPictures()); + outState.putDouble(EXTRA_SCROLL_PERCENTAGE, mTopView.getScrollPercentage()); } @Override @@ -537,6 +508,7 @@ public class MessageView extends K9Activity implements OnClickListener { mPgpData = (PgpData) savedInstanceState.getSerializable(STATE_PGP_DATA); mMessageView.updateCryptoLayout(mAccount.getCryptoProvider(), mPgpData, mMessage); mMessageView.setLoadPictures(savedInstanceState.getBoolean(SHOW_PICTURES)); + mTopView.setScrollPercentage(savedInstanceState.getDouble(EXTRA_SCROLL_PERCENTAGE)); } private void displayMessage(MessageReference ref) { @@ -656,11 +628,13 @@ public class MessageView extends K9Activity implements OnClickListener { onAccountUnavailable(); return; } + mController.addListener(mTopView.getListener()); StorageManager.getInstance(getApplication()).addListener(mStorageListener); } @Override protected void onPause() { + mController.removeListener(mTopView.getListener()); StorageManager.getInstance(getApplication()).removeListener(mStorageListener); super.onPause(); } @@ -887,6 +861,8 @@ public class MessageView extends K9Activity implements OnClickListener { } protected void onNext() { + // Reset scroll percentage when we change messages + mTopView.setScrollPercentage(0); if (mNextMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -901,6 +877,8 @@ public class MessageView extends K9Activity implements OnClickListener { } protected void onPrevious() { + // Reset scroll percentage when we change messages + mTopView.setScrollPercentage(0); if (mPreviousMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -1128,8 +1106,8 @@ public class MessageView extends K9Activity implements OnClickListener { mTopView.scrollTo(0, 0); try { if (MessageView.this.mMessage != null - && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL) - && message.isSet(Flag.X_DOWNLOADED_FULL)) { + && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL) + && message.isSet(Flag.X_DOWNLOADED_FULL)) { mMessageView.setHeaders(message, account); } MessageView.this.mMessage = message; @@ -1289,5 +1267,4 @@ public class MessageView extends K9Activity implements OnClickListener { // sometimes shows the original encrypted content mMessageView.loadBodyFromText(mAccount.getCryptoProvider(), mPgpData, mMessage, mPgpData.getDecryptedData(), "text/plain"); } - } diff --git a/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java new file mode 100644 index 000000000..f8bef5741 --- /dev/null +++ b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java @@ -0,0 +1,111 @@ +package com.fsck.k9.activity.misc; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; + +/** + * Extends {@link AsyncTask} with methods to attach and detach an {@link Activity}. + * + *

+ * This is necessary to properly handle configuration changes that will restart an activity. + *

+ * Note: + * Implementing classes need to make sure they have no reference to the {@code Activity} instance + * that created the instance of that class. So if it's implemented as inner class, it needs to be + * {@code static}. + *

+ * + * @param + * see {@link AsyncTask} + * @param + * see {@link AsyncTask} + * @param + * see {@link AsyncTask} + * + * @see #restore(Activity) + * @see #retain() + */ +public abstract class ExtendedAsyncTask + extends AsyncTask implements NonConfigurationInstance { + protected Activity mActivity; + protected Context mContext; + protected ProgressDialog mProgressDialog; + + protected ExtendedAsyncTask(Activity activity) { + mActivity = activity; + mContext = activity.getApplicationContext(); + } + + /** + * Connect this {@link AsyncTask} to a new {@link Activity} instance after the activity + * was restarted due to a configuration change. + * + *

+ * This also creates a new progress dialog that is bound to the new activity. + *

+ * + * @param activity + * The new {@code Activity} instance. Never {@code null}. + */ + @Override + public void restore(Activity activity) { + mActivity = activity; + showProgressDialog(); + } + + /** + * Detach this {@link AsyncTask} from the {@link Activity} it was bound to. + * + *

+ * This needs to be called when the current activity is being destroyed during an activity + * restart due to a configuration change.
+ * We also have to destroy the progress dialog because it's bound to the activity that's + * being destroyed. + *

+ * + * @return {@code true} if this instance should be retained; {@code false} otherwise. + * + * @see Activity#onRetainNonConfigurationInstance() + */ + @Override + public boolean retain() { + boolean retain = false; + if (mProgressDialog != null) { + removeProgressDialog(); + retain = true; + } + mActivity = null; + + return retain; + } + + /** + * Creates a {@link ProgressDialog} that is shown while the background thread is running. + * + *

+ * This needs to store a {@code ProgressDialog} instance in {@link #mProgressDialog} or + * override {@link #removeProgressDialog()}. + *

+ */ + protected abstract void showProgressDialog(); + + protected void removeProgressDialog() { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + + /** + * This default implementation only creates a progress dialog. + * + *

+ * Important: + * Be sure to call {@link #removeProgressDialog()} in {@link AsyncTask#onPostExecute(Object)}. + *

+ */ + @Override + protected void onPreExecute() { + showProgressDialog(); + } +} diff --git a/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java b/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java new file mode 100644 index 000000000..d9f314581 --- /dev/null +++ b/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java @@ -0,0 +1,38 @@ +package com.fsck.k9.activity.misc; + +import android.app.Activity; + + +public interface NonConfigurationInstance { + /** + * Decide whether to retain this {@code NonConfigurationInstance} and clean up resources if + * necessary. + * + *

+ * This needs to be called when the current activity is being destroyed during an activity + * restart due to a configuration change.
+ * Implementations should make sure that references to the {@code Activity} instance that is + * about to be destroyed are cleared to avoid memory leaks. This includes all UI elements that + * are bound to an activity (e.g. dialogs). They can be re-created in + * {@link #restore(Activity)}. + *

+ * + * @return {@code true} if this instance should be retained; {@code false} otherwise. + * + * @see Activity#onRetainNonConfigurationInstance() + */ + public boolean retain(); + + /** + * Connect this retained {@code NonConfigurationInstance} to the new {@link Activity} instance + * after the activity was restarted due to a configuration change. + * + *

+ * This also creates a new progress dialog that is bound to the new activity. + *

+ * + * @param activity + * The new {@code Activity} instance. Never {@code null}. + */ + public void restore(Activity activity); +} diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 38139bb89..1e2fbca33 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -650,15 +650,10 @@ public class MessagingController implements Runnable { accountUuidsSet.addAll(Arrays.asList(accountUuids)); } final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext()); - Account[] accounts = prefs.getAccounts(); List foldersToSearch = null; boolean displayableOnly = false; boolean noSpecialFolders = true; - for (final Account account : accounts) { - if (!account.isAvailable(mApplication)) { - Log.d(K9.LOG_TAG, "searchLocalMessagesSynchronous() ignores account that is not available"); - continue; - } + for (final Account account : prefs.getAvailableAccounts()) { if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) { continue; } @@ -2851,8 +2846,7 @@ public class MessagingController implements Runnable { public void sendPendingMessages(MessagingListener listener) { final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext()); - Account[] accounts = prefs.getAccounts(); - for (Account account : accounts) { + for (Account account : prefs.getAvailableAccounts()) { sendPendingMessages(account, listener); } } @@ -3612,13 +3606,12 @@ public class MessagingController implements Runnable { Log.i(K9.LOG_TAG, "Starting mail check"); Preferences prefs = Preferences.getPreferences(context); - Account[] accounts; + Collection accounts; if (account != null) { - accounts = new Account[] { - account - }; + accounts = new ArrayList(1); + accounts.add(account); } else { - accounts = prefs.getAccounts(); + accounts = prefs.getAvailableAccounts(); } for (final Account account : accounts) { diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java index bed951a2a..bdf35fa00 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -112,6 +112,11 @@ public class MessagingListener { public void loadMessageForViewFailed(Account account, String folder, String uid, Throwable t) { } + /** + * Called when a message for view has been fully displayed on the screen. + */ + public void messageViewFinished() {} + public void checkMailStarted(Context context, Account account) { } diff --git a/src/com/fsck/k9/helper/DateFormatter.java b/src/com/fsck/k9/helper/DateFormatter.java index 12a9e1a31..784195f37 100644 --- a/src/com/fsck/k9/helper/DateFormatter.java +++ b/src/com/fsck/k9/helper/DateFormatter.java @@ -43,6 +43,10 @@ public class DateFormatter { } }; + public static void clearChosenFormat() { + sChosenFormat = null; + } + public static DateFormat getDateFormat(Context context, String formatString) { java.text.DateFormat dateFormat; diff --git a/src/com/fsck/k9/mail/ConnectionSecurity.java b/src/com/fsck/k9/mail/ConnectionSecurity.java new file mode 100644 index 000000000..98741303e --- /dev/null +++ b/src/com/fsck/k9/mail/ConnectionSecurity.java @@ -0,0 +1,19 @@ +package com.fsck.k9.mail; + +/** + * The currently available connection security types. + * + *

+ * Right now this enum is only used by {@link ServerSettings} and converted to store- or + * transport-specific constants in the different {@link Store} and {@link Transport} + * implementations. In the future we probably want to change this and use + * {@code ConnectionSecurity} exclusively. + *

+ */ +public enum ConnectionSecurity { + NONE, + STARTTLS_OPTIONAL, + STARTTLS_REQUIRED, + SSL_TLS_OPTIONAL, + SSL_TLS_REQUIRED +} diff --git a/src/com/fsck/k9/mail/ServerSettings.java b/src/com/fsck/k9/mail/ServerSettings.java new file mode 100644 index 000000000..f7f127d29 --- /dev/null +++ b/src/com/fsck/k9/mail/ServerSettings.java @@ -0,0 +1,133 @@ +package com.fsck.k9.mail; + +import java.util.Map; +import com.fsck.k9.Account; + +/** + * This is an abstraction to get rid of the store- and transport-specific URIs. + * + *

+ * Right now it's only used for settings import/export. But the goal is to get rid of + * store/transport URIs altogether. + *

+ * + * @see Account#getStoreUri() + * @see Account#getTransportUri() + */ +public class ServerSettings { + /** + * Name of the store or transport type (e.g. "IMAP"). + */ + public final String type; + + /** + * The host name of the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String host; + + /** + * The port number of the server. + * + * {@code -1} if not applicable for the store or transport. + */ + public final int port; + + /** + * The type of connection security to be used when connecting to the server. + * + * {@link ConnectionSecurity#NONE} if not applicable for the store or transport. + */ + public final ConnectionSecurity connectionSecurity; + + /** + * The authentication method to use when connecting to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String authenticationType; + + /** + * The username part of the credentials needed to authenticate to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String username; + + /** + * The password part of the credentials needed to authenticate to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String password; + + + /** + * Creates a new {@code ServerSettings} object. + * + * @param type + * see {@link ServerSettings#type} + * @param host + * see {@link ServerSettings#host} + * @param port + * see {@link ServerSettings#port} + * @param connectionSecurity + * see {@link ServerSettings#connectionSecurity} + * @param authenticationType + * see {@link ServerSettings#authenticationType} + * @param username + * see {@link ServerSettings#username} + * @param password + * see {@link ServerSettings#password} + */ + public ServerSettings(String type, String host, int port, + ConnectionSecurity connectionSecurity, String authenticationType, String username, + String password) { + this.type = type; + this.host = host; + this.port = port; + this.connectionSecurity = connectionSecurity; + this.authenticationType = authenticationType; + this.username = username; + this.password = password; + } + + /** + * Creates an "empty" {@code ServerSettings} object. + * + * Everything but {@link ServerSettings#type} is unused. + * + * @param type + * see {@link ServerSettings#type} + */ + public ServerSettings(String type) { + this.type = type; + host = null; + port = -1; + connectionSecurity = ConnectionSecurity.NONE; + authenticationType = null; + username = null; + password = null; + } + + /** + * Returns store- or transport-specific settings as key/value pair. + * + *

Classes that inherit from this one are expected to override this method.

+ */ + public Map getExtra() { + return null; + } + + protected void putIfNotNull(Map map, String key, String value) { + if (value != null) { + map.put(key, value); + } + } + + public ServerSettings newPassword(String newPassword) { + return new ServerSettings(type, host, port, connectionSecurity, authenticationType, + username, newPassword); + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 07bfb24d8..c06be7c44 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -31,17 +31,13 @@ public abstract class Store { /** * Remote stores indexed by Uri. */ - private static HashMap mStores = new HashMap(); + private static HashMap sStores = new HashMap(); + /** * Local stores indexed by UUid because the Uri may change due to migration to/from SD-card. */ - private static HashMap mLocalStores = new HashMap(); + private static HashMap sLocalStores = new HashMap(); - protected final Account mAccount; - - protected Store(Account account) { - mAccount = account; - } /** * Get an instance of a remote mail store. @@ -53,7 +49,7 @@ public abstract class Store { throw new RuntimeException("Asked to get non-local Store object but given LocalStore URI"); } - Store store = mStores.get(uri); + Store store = sStores.get(uri); if (store == null) { if (uri.startsWith("imap")) { store = new ImapStore(account); @@ -66,7 +62,7 @@ public abstract class Store { } if (store != null) { - mStores.put(uri, store); + sStores.put(uri, store); } } @@ -82,15 +78,72 @@ public abstract class Store { * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} */ public synchronized static LocalStore getLocalInstance(Account account, Application application) throws MessagingException { - Store store = mLocalStores.get(account.getUuid()); + Store store = sLocalStores.get(account.getUuid()); if (store == null) { store = new LocalStore(account, application); - mLocalStores.put(account.getUuid(), store); + sLocalStores.put(account.getUuid(), store); } return (LocalStore) store; } + /** + * Decodes the contents of store-specific URIs and puts them into a {@link ServerSettings} + * object. + * + * @param uri + * the store-specific URI to decode + * + * @return A {@link ServerSettings} object holding the settings contained in the URI. + * + * @see ImapStore#decodeUri(String) + * @see Pop3Store#decodeUri(String) + * @see WebDavStore#decodeUri(String) + */ + public static ServerSettings decodeStoreUri(String uri) { + if (uri.startsWith("imap")) { + return ImapStore.decodeUri(uri); + } else if (uri.startsWith("pop3")) { + return Pop3Store.decodeUri(uri); + } else if (uri.startsWith("webdav")) { + return WebDavStore.decodeUri(uri); + } else { + throw new IllegalArgumentException("Not a valid store URI"); + } + } + + /** + * Creates a store URI from the information supplied in the {@link ServerSettings} object. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A store URI that holds the same information as the {@code server} parameter. + * + * @see ImapStore#createUri(ServerSettings) + * @see Pop3Store#createUri(ServerSettings) + * @see WebDavStore#createUri(ServerSettings) + */ + public static String createStoreUri(ServerSettings server) { + if (ImapStore.STORE_TYPE.equals(server.type)) { + return ImapStore.createUri(server); + } else if (Pop3Store.STORE_TYPE.equals(server.type)) { + return Pop3Store.createUri(server); + } else if (WebDavStore.STORE_TYPE.equals(server.type)) { + return WebDavStore.createUri(server); + } else { + throw new IllegalArgumentException("Not a valid store URI"); + } + } + + + protected final Account mAccount; + + + protected Store(Account account) { + mAccount = account; + } + public abstract Folder getFolder(String name); public abstract List getPersonalNamespaces(boolean forceListAll) throws MessagingException; @@ -100,20 +153,23 @@ public abstract class Store { public boolean isCopyCapable() { return false; } + public boolean isMoveCapable() { return false; } + public boolean isPushCapable() { return false; } + public boolean isSendCapable() { return false; } + public boolean isExpungeCapable() { return false; } - public void sendMessages(Message[] messages) throws MessagingException { } diff --git a/src/com/fsck/k9/mail/Transport.java b/src/com/fsck/k9/mail/Transport.java index bba666e74..3429b84d7 100644 --- a/src/com/fsck/k9/mail/Transport.java +++ b/src/com/fsck/k9/mail/Transport.java @@ -25,6 +25,50 @@ public abstract class Transport { } } + /** + * Decodes the contents of transport-specific URIs and puts them into a {@link ServerSettings} + * object. + * + * @param uri + * the transport-specific URI to decode + * + * @return A {@link ServerSettings} object holding the settings contained in the URI. + * + * @see SmtpTransport#decodeUri(String) + * @see WebDavTransport#decodeUri(String) + */ + public static ServerSettings decodeTransportUri(String uri) { + if (uri.startsWith("smtp")) { + return SmtpTransport.decodeUri(uri); + } else if (uri.startsWith("webdav")) { + return WebDavTransport.decodeUri(uri); + } else { + throw new IllegalArgumentException("Not a valid transport URI"); + } + } + + /** + * Creates a transport URI from the information supplied in the {@link ServerSettings} object. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A transport URI that holds the same information as the {@code server} parameter. + * + * @see SmtpTransport#createUri(ServerSettings) + * @see WebDavTransport#createUri(ServerSettings) + */ + public static String createTransportUri(ServerSettings server) { + if (SmtpTransport.TRANSPORT_TYPE.equals(server.type)) { + return SmtpTransport.createUri(server); + } else if (WebDavTransport.TRANSPORT_TYPE.equals(server.type)) { + return WebDavTransport.createUri(server); + } else { + throw new IllegalArgumentException("Not a valid transport URI"); + } + } + + public abstract void open() throws MessagingException; public abstract void sendMessage(Message message) throws MessagingException; diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 2922a8bd7..8371de8c9 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -21,7 +21,7 @@ import java.nio.charset.IllegalCharsetNameException; public class MimeUtility { public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; - + public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; /* * http://www.w3schools.com/media/media_mimeref.asp @@ -29,853 +29,856 @@ public class MimeUtility { * http://www.stdicon.com/mimetypes */ public static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { - { "", "application/octet-stream" }, - { "123", "application/vnd.lotus-1-2-3"}, - { "323", "text/h323"}, - { "3dml", "text/vnd.in3d.3dml"}, - { "3g2", "video/3gpp2"}, - { "3gp", "video/3gpp"}, - { "aab", "application/x-authorware-bin"}, - { "aac", "audio/x-aac"}, - { "aam", "application/x-authorware-map"}, - { "a", "application/octet-stream"}, - { "aas", "application/x-authorware-seg"}, - { "abw", "application/x-abiword"}, - { "acc", "application/vnd.americandynamics.acc"}, - { "ace", "application/x-ace-compressed"}, - { "acu", "application/vnd.acucobol"}, - { "acutc", "application/vnd.acucorp"}, - { "acx", "application/internet-property-stream"}, - { "adp", "audio/adpcm"}, - { "aep", "application/vnd.audiograph"}, - { "afm", "application/x-font-type1"}, - { "afp", "application/vnd.ibm.modcap"}, - { "ai", "application/postscript"}, - { "aif", "audio/x-aiff"}, - { "aifc", "audio/x-aiff"}, - { "aiff", "audio/x-aiff"}, - { "air", "application/vnd.adobe.air-application-installer-package+zip"}, - { "ami", "application/vnd.amiga.ami"}, - { "apk", "application/vnd.android.package-archive"}, - { "application", "application/x-ms-application"}, - { "apr", "application/vnd.lotus-approach"}, - { "asc", "application/pgp-signature"}, - { "asf", "video/x-ms-asf"}, - { "asm", "text/x-asm"}, - { "aso", "application/vnd.accpac.simply.aso"}, - { "asr", "video/x-ms-asf"}, - { "asx", "video/x-ms-asf"}, - { "atc", "application/vnd.acucorp"}, - { "atom", "application/atom+xml"}, - { "atomcat", "application/atomcat+xml"}, - { "atomsvc", "application/atomsvc+xml"}, - { "atx", "application/vnd.antix.game-component"}, - { "au", "audio/basic"}, - { "avi", "video/x-msvideo"}, - { "aw", "application/applixware"}, - { "axs", "application/olescript"}, - { "azf", "application/vnd.airzip.filesecure.azf"}, - { "azs", "application/vnd.airzip.filesecure.azs"}, - { "azw", "application/vnd.amazon.ebook"}, - { "bas", "text/plain"}, - { "bat", "application/x-msdownload"}, - { "bcpio", "application/x-bcpio"}, - { "bdf", "application/x-font-bdf"}, - { "bdm", "application/vnd.syncml.dm+wbxml"}, - { "bh2", "application/vnd.fujitsu.oasysprs"}, - { "bin", "application/octet-stream"}, - { "bmi", "application/vnd.bmi"}, - { "bmp", "image/bmp"}, - { "book", "application/vnd.framemaker"}, - { "box", "application/vnd.previewsystems.box"}, - { "boz", "application/x-bzip2"}, - { "bpk", "application/octet-stream"}, - { "btif", "image/prs.btif"}, - { "bz2", "application/x-bzip2"}, - { "bz", "application/x-bzip"}, - { "c4d", "application/vnd.clonk.c4group"}, - { "c4f", "application/vnd.clonk.c4group"}, - { "c4g", "application/vnd.clonk.c4group"}, - { "c4p", "application/vnd.clonk.c4group"}, - { "c4u", "application/vnd.clonk.c4group"}, - { "cab", "application/vnd.ms-cab-compressed"}, - { "car", "application/vnd.curl.car"}, - { "cat", "application/vnd.ms-pki.seccat"}, - { "cct", "application/x-director"}, - { "cc", "text/x-c"}, - { "ccxml", "application/ccxml+xml"}, - { "cdbcmsg", "application/vnd.contact.cmsg"}, - { "cdf", "application/x-cdf"}, - { "cdkey", "application/vnd.mediastation.cdkey"}, - { "cdx", "chemical/x-cdx"}, - { "cdxml", "application/vnd.chemdraw+xml"}, - { "cdy", "application/vnd.cinderella"}, - { "cer", "application/x-x509-ca-cert"}, - { "cgm", "image/cgm"}, - { "chat", "application/x-chat"}, - { "chm", "application/vnd.ms-htmlhelp"}, - { "chrt", "application/vnd.kde.kchart"}, - { "cif", "chemical/x-cif"}, - { "cii", "application/vnd.anser-web-certificate-issue-initiation"}, - { "cla", "application/vnd.claymore"}, - { "class", "application/java-vm"}, - { "clkk", "application/vnd.crick.clicker.keyboard"}, - { "clkp", "application/vnd.crick.clicker.palette"}, - { "clkt", "application/vnd.crick.clicker.template"}, - { "clkw", "application/vnd.crick.clicker.wordbank"}, - { "clkx", "application/vnd.crick.clicker"}, - { "clp", "application/x-msclip"}, - { "cmc", "application/vnd.cosmocaller"}, - { "cmdf", "chemical/x-cmdf"}, - { "cml", "chemical/x-cml"}, - { "cmp", "application/vnd.yellowriver-custom-menu"}, - { "cmx", "image/x-cmx"}, - { "cod", "application/vnd.rim.cod"}, - { "com", "application/x-msdownload"}, - { "conf", "text/plain"}, - { "cpio", "application/x-cpio"}, - { "cpp", "text/x-c"}, - { "cpt", "application/mac-compactpro"}, - { "crd", "application/x-mscardfile"}, - { "crl", "application/pkix-crl"}, - { "crt", "application/x-x509-ca-cert"}, - { "csh", "application/x-csh"}, - { "csml", "chemical/x-csml"}, - { "csp", "application/vnd.commonspace"}, - { "css", "text/css"}, - { "cst", "application/x-director"}, - { "csv", "text/csv"}, - { "c", "text/plain"}, - { "cu", "application/cu-seeme"}, - { "curl", "text/vnd.curl"}, - { "cww", "application/prs.cww"}, - { "cxt", "application/x-director"}, - { "cxx", "text/x-c"}, - { "daf", "application/vnd.mobius.daf"}, - { "dataless", "application/vnd.fdsn.seed"}, - { "davmount", "application/davmount+xml"}, - { "dcr", "application/x-director"}, - { "dcurl", "text/vnd.curl.dcurl"}, - { "dd2", "application/vnd.oma.dd2+xml"}, - { "ddd", "application/vnd.fujixerox.ddd"}, - { "deb", "application/x-debian-package"}, - { "def", "text/plain"}, - { "deploy", "application/octet-stream"}, - { "der", "application/x-x509-ca-cert"}, - { "dfac", "application/vnd.dreamfactory"}, - { "dic", "text/x-c"}, - { "diff", "text/plain"}, - { "dir", "application/x-director"}, - { "dis", "application/vnd.mobius.dis"}, - { "dist", "application/octet-stream"}, - { "distz", "application/octet-stream"}, - { "djv", "image/vnd.djvu"}, - { "djvu", "image/vnd.djvu"}, - { "dll", "application/x-msdownload"}, - { "dmg", "application/octet-stream"}, - { "dms", "application/octet-stream"}, - { "dna", "application/vnd.dna"}, - { "doc", "application/msword"}, - { "docm", "application/vnd.ms-word.document.macroenabled.12"}, - { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - { "dot", "application/msword"}, - { "dotm", "application/vnd.ms-word.template.macroenabled.12"}, - { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, - { "dp", "application/vnd.osgi.dp"}, - { "dpg", "application/vnd.dpgraph"}, - { "dsc", "text/prs.lines.tag"}, - { "dtb", "application/x-dtbook+xml"}, - { "dtd", "application/xml-dtd"}, - { "dts", "audio/vnd.dts"}, - { "dtshd", "audio/vnd.dts.hd"}, - { "dump", "application/octet-stream"}, - { "dvi", "application/x-dvi"}, - { "dwf", "model/vnd.dwf"}, - { "dwg", "image/vnd.dwg"}, - { "dxf", "image/vnd.dxf"}, - { "dxp", "application/vnd.spotfire.dxp"}, - { "dxr", "application/x-director"}, - { "ecelp4800", "audio/vnd.nuera.ecelp4800"}, - { "ecelp7470", "audio/vnd.nuera.ecelp7470"}, - { "ecelp9600", "audio/vnd.nuera.ecelp9600"}, - { "ecma", "application/ecmascript"}, - { "edm", "application/vnd.novadigm.edm"}, - { "edx", "application/vnd.novadigm.edx"}, - { "efif", "application/vnd.picsel"}, - { "ei6", "application/vnd.pg.osasli"}, - { "elc", "application/octet-stream"}, - { "eml", "message/rfc822"}, - { "emma", "application/emma+xml"}, - { "eol", "audio/vnd.digital-winds"}, - { "eot", "application/vnd.ms-fontobject"}, - { "eps", "application/postscript"}, - { "epub", "application/epub+zip"}, - { "es3", "application/vnd.eszigno3+xml"}, - { "esf", "application/vnd.epson.esf"}, - { "et3", "application/vnd.eszigno3+xml"}, - { "etx", "text/x-setext"}, - { "evy", "application/envoy"}, - { "exe", "application/octet-stream"}, - { "ext", "application/vnd.novadigm.ext"}, - { "ez2", "application/vnd.ezpix-album"}, - { "ez3", "application/vnd.ezpix-package"}, - { "ez", "application/andrew-inset"}, - { "f4v", "video/x-f4v"}, - { "f77", "text/x-fortran"}, - { "f90", "text/x-fortran"}, - { "fbs", "image/vnd.fastbidsheet"}, - { "fdf", "application/vnd.fdf"}, - { "fe_launch", "application/vnd.denovo.fcselayout-link"}, - { "fg5", "application/vnd.fujitsu.oasysgp"}, - { "fgd", "application/x-director"}, - { "fh4", "image/x-freehand"}, - { "fh5", "image/x-freehand"}, - { "fh7", "image/x-freehand"}, - { "fhc", "image/x-freehand"}, - { "fh", "image/x-freehand"}, - { "fif", "application/fractals"}, - { "fig", "application/x-xfig"}, - { "fli", "video/x-fli"}, - { "flo", "application/vnd.micrografx.flo"}, - { "flr", "x-world/x-vrml"}, - { "flv", "video/x-flv"}, - { "flw", "application/vnd.kde.kivio"}, - { "flx", "text/vnd.fmi.flexstor"}, - { "fly", "text/vnd.fly"}, - { "fm", "application/vnd.framemaker"}, - { "fnc", "application/vnd.frogans.fnc"}, - { "for", "text/x-fortran"}, - { "fpx", "image/vnd.fpx"}, - { "frame", "application/vnd.framemaker"}, - { "fsc", "application/vnd.fsc.weblaunch"}, - { "fst", "image/vnd.fst"}, - { "ftc", "application/vnd.fluxtime.clip"}, - { "f", "text/x-fortran"}, - { "fti", "application/vnd.anser-web-funds-transfer-initiation"}, - { "fvt", "video/vnd.fvt"}, - { "fzs", "application/vnd.fuzzysheet"}, - { "g3", "image/g3fax"}, - { "gac", "application/vnd.groove-account"}, - { "gdl", "model/vnd.gdl"}, - { "geo", "application/vnd.dynageo"}, - { "gex", "application/vnd.geometry-explorer"}, - { "ggb", "application/vnd.geogebra.file"}, - { "ggt", "application/vnd.geogebra.tool"}, - { "ghf", "application/vnd.groove-help"}, - { "gif", "image/gif"}, - { "gim", "application/vnd.groove-identity-message"}, - { "gmx", "application/vnd.gmx"}, - { "gnumeric", "application/x-gnumeric"}, - { "gph", "application/vnd.flographit"}, - { "gqf", "application/vnd.grafeq"}, - { "gqs", "application/vnd.grafeq"}, - { "gram", "application/srgs"}, - { "gre", "application/vnd.geometry-explorer"}, - { "grv", "application/vnd.groove-injector"}, - { "grxml", "application/srgs+xml"}, - { "gsf", "application/x-font-ghostscript"}, - { "gtar", "application/x-gtar"}, - { "gtm", "application/vnd.groove-tool-message"}, - { "gtw", "model/vnd.gtw"}, - { "gv", "text/vnd.graphviz"}, - { "gz", "application/x-gzip"}, - { "h261", "video/h261"}, - { "h263", "video/h263"}, - { "h264", "video/h264"}, - { "hbci", "application/vnd.hbci"}, - { "hdf", "application/x-hdf"}, - { "hh", "text/x-c"}, - { "hlp", "application/winhlp"}, - { "hpgl", "application/vnd.hp-hpgl"}, - { "hpid", "application/vnd.hp-hpid"}, - { "hps", "application/vnd.hp-hps"}, - { "hqx", "application/mac-binhex40"}, - { "hta", "application/hta"}, - { "htc", "text/x-component"}, - { "h", "text/plain"}, - { "htke", "application/vnd.kenameaapp"}, - { "html", "text/html"}, - { "htm", "text/html"}, - { "htt", "text/webviewhtml"}, - { "hvd", "application/vnd.yamaha.hv-dic"}, - { "hvp", "application/vnd.yamaha.hv-voice"}, - { "hvs", "application/vnd.yamaha.hv-script"}, - { "icc", "application/vnd.iccprofile"}, - { "ice", "x-conference/x-cooltalk"}, - { "icm", "application/vnd.iccprofile"}, - { "ico", "image/x-icon"}, - { "ics", "text/calendar"}, - { "ief", "image/ief"}, - { "ifb", "text/calendar"}, - { "ifm", "application/vnd.shana.informed.formdata"}, - { "iges", "model/iges"}, - { "igl", "application/vnd.igloader"}, - { "igs", "model/iges"}, - { "igx", "application/vnd.micrografx.igx"}, - { "iif", "application/vnd.shana.informed.interchange"}, - { "iii", "application/x-iphone"}, - { "imp", "application/vnd.accpac.simply.imp"}, - { "ims", "application/vnd.ms-ims"}, - { "ins", "application/x-internet-signup"}, - { "in", "text/plain"}, - { "ipk", "application/vnd.shana.informed.package"}, - { "irm", "application/vnd.ibm.rights-management"}, - { "irp", "application/vnd.irepository.package+xml"}, - { "iso", "application/octet-stream"}, - { "isp", "application/x-internet-signup"}, - { "itp", "application/vnd.shana.informed.formtemplate"}, - { "ivp", "application/vnd.immervision-ivp"}, - { "ivu", "application/vnd.immervision-ivu"}, - { "jad", "text/vnd.sun.j2me.app-descriptor"}, - { "jam", "application/vnd.jam"}, - { "jar", "application/java-archive"}, - { "java", "text/x-java-source"}, - { "jfif", "image/pipeg"}, - { "jisp", "application/vnd.jisp"}, - { "jlt", "application/vnd.hp-jlyt"}, - { "jnlp", "application/x-java-jnlp-file"}, - { "joda", "application/vnd.joost.joda-archive"}, - { "jpeg", "image/jpeg"}, - { "jpe", "image/jpeg"}, - { "jpg", "image/jpeg"}, - { "jpgm", "video/jpm"}, - { "jpgv", "video/jpeg"}, - { "jpm", "video/jpm"}, - { "js", "application/x-javascript"}, - { "json", "application/json"}, - { "kar", "audio/midi"}, - { "karbon", "application/vnd.kde.karbon"}, - { "kfo", "application/vnd.kde.kformula"}, - { "kia", "application/vnd.kidspiration"}, - { "kil", "application/x-killustrator"}, - { "kml", "application/vnd.google-earth.kml+xml"}, - { "kmz", "application/vnd.google-earth.kmz"}, - { "kne", "application/vnd.kinar"}, - { "knp", "application/vnd.kinar"}, - { "kon", "application/vnd.kde.kontour"}, - { "kpr", "application/vnd.kde.kpresenter"}, - { "kpt", "application/vnd.kde.kpresenter"}, - { "ksh", "text/plain"}, - { "ksp", "application/vnd.kde.kspread"}, - { "ktr", "application/vnd.kahootz"}, - { "ktz", "application/vnd.kahootz"}, - { "kwd", "application/vnd.kde.kword"}, - { "kwt", "application/vnd.kde.kword"}, - { "latex", "application/x-latex"}, - { "lbd", "application/vnd.llamagraphics.life-balance.desktop"}, - { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, - { "les", "application/vnd.hhe.lesson-player"}, - { "lha", "application/octet-stream"}, - { "link66", "application/vnd.route66.link66+xml"}, - { "list3820", "application/vnd.ibm.modcap"}, - { "listafp", "application/vnd.ibm.modcap"}, - { "list", "text/plain"}, - { "log", "text/plain"}, - { "lostxml", "application/lost+xml"}, - { "lrf", "application/octet-stream"}, - { "lrm", "application/vnd.ms-lrm"}, - { "lsf", "video/x-la-asf"}, - { "lsx", "video/x-la-asf"}, - { "ltf", "application/vnd.frogans.ltf"}, - { "lvp", "audio/vnd.lucent.voice"}, - { "lwp", "application/vnd.lotus-wordpro"}, - { "lzh", "application/octet-stream"}, - { "m13", "application/x-msmediaview"}, - { "m14", "application/x-msmediaview"}, - { "m1v", "video/mpeg"}, - { "m2a", "audio/mpeg"}, - { "m2v", "video/mpeg"}, - { "m3a", "audio/mpeg"}, - { "m3u", "audio/x-mpegurl"}, - { "m4u", "video/vnd.mpegurl"}, - { "m4v", "video/x-m4v"}, - { "ma", "application/mathematica"}, - { "mag", "application/vnd.ecowin.chart"}, - { "maker", "application/vnd.framemaker"}, - { "man", "text/troff"}, - { "mathml", "application/mathml+xml"}, - { "mb", "application/mathematica"}, - { "mbk", "application/vnd.mobius.mbk"}, - { "mbox", "application/mbox"}, - { "mc1", "application/vnd.medcalcdata"}, - { "mcd", "application/vnd.mcd"}, - { "mcurl", "text/vnd.curl.mcurl"}, - { "mdb", "application/x-msaccess"}, - { "mdi", "image/vnd.ms-modi"}, - { "mesh", "model/mesh"}, - { "me", "text/troff"}, - { "mfm", "application/vnd.mfmp"}, - { "mgz", "application/vnd.proteus.magazine"}, - { "mht", "message/rfc822"}, - { "mhtml", "message/rfc822"}, - { "mid", "audio/midi"}, - { "midi", "audio/midi"}, - { "mif", "application/vnd.mif"}, - { "mime", "message/rfc822"}, - { "mj2", "video/mj2"}, - { "mjp2", "video/mj2"}, - { "mlp", "application/vnd.dolby.mlp"}, - { "mmd", "application/vnd.chipnuts.karaoke-mmd"}, - { "mmf", "application/vnd.smaf"}, - { "mmr", "image/vnd.fujixerox.edmics-mmr"}, - { "mny", "application/x-msmoney"}, - { "mobi", "application/x-mobipocket-ebook"}, - { "movie", "video/x-sgi-movie"}, - { "mov", "video/quicktime"}, - { "mp2a", "audio/mpeg"}, - { "mp2", "video/mpeg"}, - { "mp3", "audio/mpeg"}, - { "mp4a", "audio/mp4"}, - { "mp4s", "application/mp4"}, - { "mp4", "video/mp4"}, - { "mp4v", "video/mp4"}, - { "mpa", "video/mpeg"}, - { "mpc", "application/vnd.mophun.certificate"}, - { "mpeg", "video/mpeg"}, - { "mpe", "video/mpeg"}, - { "mpg4", "video/mp4"}, - { "mpga", "audio/mpeg"}, - { "mpg", "video/mpeg"}, - { "mpkg", "application/vnd.apple.installer+xml"}, - { "mpm", "application/vnd.blueice.multipass"}, - { "mpn", "application/vnd.mophun.application"}, - { "mpp", "application/vnd.ms-project"}, - { "mpt", "application/vnd.ms-project"}, - { "mpv2", "video/mpeg"}, - { "mpy", "application/vnd.ibm.minipay"}, - { "mqy", "application/vnd.mobius.mqy"}, - { "mrc", "application/marc"}, - { "mscml", "application/mediaservercontrol+xml"}, - { "mseed", "application/vnd.fdsn.mseed"}, - { "mseq", "application/vnd.mseq"}, - { "msf", "application/vnd.epson.msf"}, - { "msh", "model/mesh"}, - { "msi", "application/x-msdownload"}, - { "ms", "text/troff"}, - { "msty", "application/vnd.muvee.style"}, - { "mts", "model/vnd.mts"}, - { "mus", "application/vnd.musician"}, - { "musicxml", "application/vnd.recordare.musicxml+xml"}, - { "mvb", "application/x-msmediaview"}, - { "mxf", "application/mxf"}, - { "mxl", "application/vnd.recordare.musicxml"}, - { "mxml", "application/xv+xml"}, - { "mxs", "application/vnd.triscape.mxs"}, - { "mxu", "video/vnd.mpegurl"}, - { "nb", "application/mathematica"}, - { "nc", "application/x-netcdf"}, - { "ncx", "application/x-dtbncx+xml"}, - { "n-gage", "application/vnd.nokia.n-gage.symbian.install"}, - { "ngdat", "application/vnd.nokia.n-gage.data"}, - { "nlu", "application/vnd.neurolanguage.nlu"}, - { "nml", "application/vnd.enliven"}, - { "nnd", "application/vnd.noblenet-directory"}, - { "nns", "application/vnd.noblenet-sealer"}, - { "nnw", "application/vnd.noblenet-web"}, - { "npx", "image/vnd.net-fpx"}, - { "nsf", "application/vnd.lotus-notes"}, - { "nws", "message/rfc822"}, - { "oa2", "application/vnd.fujitsu.oasys2"}, - { "oa3", "application/vnd.fujitsu.oasys3"}, - { "o", "application/octet-stream"}, - { "oas", "application/vnd.fujitsu.oasys"}, - { "obd", "application/x-msbinder"}, - { "obj", "application/octet-stream"}, - { "oda", "application/oda"}, - { "odb", "application/vnd.oasis.opendocument.database"}, - { "odc", "application/vnd.oasis.opendocument.chart"}, - { "odf", "application/vnd.oasis.opendocument.formula"}, - { "odft", "application/vnd.oasis.opendocument.formula-template"}, - { "odg", "application/vnd.oasis.opendocument.graphics"}, - { "odi", "application/vnd.oasis.opendocument.image"}, - { "odp", "application/vnd.oasis.opendocument.presentation"}, - { "ods", "application/vnd.oasis.opendocument.spreadsheet"}, - { "odt", "application/vnd.oasis.opendocument.text"}, - { "oga", "audio/ogg"}, - { "ogg", "audio/ogg"}, - { "ogv", "video/ogg"}, - { "ogx", "application/ogg"}, - { "onepkg", "application/onenote"}, - { "onetmp", "application/onenote"}, - { "onetoc2", "application/onenote"}, - { "onetoc", "application/onenote"}, - { "opf", "application/oebps-package+xml"}, - { "oprc", "application/vnd.palm"}, - { "org", "application/vnd.lotus-organizer"}, - { "osf", "application/vnd.yamaha.openscoreformat"}, - { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, - { "otc", "application/vnd.oasis.opendocument.chart-template"}, - { "otf", "application/x-font-otf"}, - { "otg", "application/vnd.oasis.opendocument.graphics-template"}, - { "oth", "application/vnd.oasis.opendocument.text-web"}, - { "oti", "application/vnd.oasis.opendocument.image-template"}, - { "otm", "application/vnd.oasis.opendocument.text-master"}, - { "otp", "application/vnd.oasis.opendocument.presentation-template"}, - { "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, - { "ott", "application/vnd.oasis.opendocument.text-template"}, - { "oxt", "application/vnd.openofficeorg.extension"}, - { "p10", "application/pkcs10"}, - { "p12", "application/x-pkcs12"}, - { "p7b", "application/x-pkcs7-certificates"}, - { "p7c", "application/x-pkcs7-mime"}, - { "p7m", "application/x-pkcs7-mime"}, - { "p7r", "application/x-pkcs7-certreqresp"}, - { "p7s", "application/x-pkcs7-signature"}, - { "pas", "text/x-pascal"}, - { "pbd", "application/vnd.powerbuilder6"}, - { "pbm", "image/x-portable-bitmap"}, - { "pcf", "application/x-font-pcf"}, - { "pcl", "application/vnd.hp-pcl"}, - { "pclxl", "application/vnd.hp-pclxl"}, - { "pct", "image/x-pict"}, - { "pcurl", "application/vnd.curl.pcurl"}, - { "pcx", "image/x-pcx"}, - { "pdb", "application/vnd.palm"}, - { "pdf", "application/pdf"}, - { "pfa", "application/x-font-type1"}, - { "pfb", "application/x-font-type1"}, - { "pfm", "application/x-font-type1"}, - { "pfr", "application/font-tdpfr"}, - { "pfx", "application/x-pkcs12"}, - { "pgm", "image/x-portable-graymap"}, - { "pgn", "application/x-chess-pgn"}, - { "pgp", "application/pgp-encrypted"}, - { "pic", "image/x-pict"}, - { "pkg", "application/octet-stream"}, - { "pki", "application/pkixcmp"}, - { "pkipath", "application/pkix-pkipath"}, - { "pko", "application/ynd.ms-pkipko"}, - { "plb", "application/vnd.3gpp.pic-bw-large"}, - { "plc", "application/vnd.mobius.plc"}, - { "plf", "application/vnd.pocketlearn"}, - { "pls", "application/pls+xml"}, - { "pl", "text/plain"}, - { "pma", "application/x-perfmon"}, - { "pmc", "application/x-perfmon"}, - { "pml", "application/x-perfmon"}, - { "pmr", "application/x-perfmon"}, - { "pmw", "application/x-perfmon"}, - { "png", "image/png"}, - { "pnm", "image/x-portable-anymap"}, - { "portpkg", "application/vnd.macports.portpkg"}, - { "pot,", "application/vnd.ms-powerpoint"}, - { "pot", "application/vnd.ms-powerpoint"}, - { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, - { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, - { "ppa", "application/vnd.ms-powerpoint"}, - { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, - { "ppd", "application/vnd.cups-ppd"}, - { "ppm", "image/x-portable-pixmap"}, - { "pps", "application/vnd.ms-powerpoint"}, - { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, - { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, - { "ppt", "application/vnd.ms-powerpoint"}, - { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, - { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - { "pqa", "application/vnd.palm"}, - { "prc", "application/x-mobipocket-ebook"}, - { "pre", "application/vnd.lotus-freelance"}, - { "prf", "application/pics-rules"}, - { "ps", "application/postscript"}, - { "psb", "application/vnd.3gpp.pic-bw-small"}, - { "psd", "image/vnd.adobe.photoshop"}, - { "psf", "application/x-font-linux-psf"}, - { "p", "text/x-pascal"}, - { "ptid", "application/vnd.pvi.ptid1"}, - { "pub", "application/x-mspublisher"}, - { "pvb", "application/vnd.3gpp.pic-bw-var"}, - { "pwn", "application/vnd.3m.post-it-notes"}, - { "pwz", "application/vnd.ms-powerpoint"}, - { "pya", "audio/vnd.ms-playready.media.pya"}, - { "pyc", "application/x-python-code"}, - { "pyo", "application/x-python-code"}, - { "py", "text/x-python"}, - { "pyv", "video/vnd.ms-playready.media.pyv"}, - { "qam", "application/vnd.epson.quickanime"}, - { "qbo", "application/vnd.intu.qbo"}, - { "qfx", "application/vnd.intu.qfx"}, - { "qps", "application/vnd.publishare-delta-tree"}, - { "qt", "video/quicktime"}, - { "qwd", "application/vnd.quark.quarkxpress"}, - { "qwt", "application/vnd.quark.quarkxpress"}, - { "qxb", "application/vnd.quark.quarkxpress"}, - { "qxd", "application/vnd.quark.quarkxpress"}, - { "qxl", "application/vnd.quark.quarkxpress"}, - { "qxt", "application/vnd.quark.quarkxpress"}, - { "ra", "audio/x-pn-realaudio"}, - { "ram", "audio/x-pn-realaudio"}, - { "rar", "application/x-rar-compressed"}, - { "ras", "image/x-cmu-raster"}, - { "rcprofile", "application/vnd.ipunplugged.rcprofile"}, - { "rdf", "application/rdf+xml"}, - { "rdz", "application/vnd.data-vision.rdz"}, - { "rep", "application/vnd.businessobjects"}, - { "res", "application/x-dtbresource+xml"}, - { "rgb", "image/x-rgb"}, - { "rif", "application/reginfo+xml"}, - { "rl", "application/resource-lists+xml"}, - { "rlc", "image/vnd.fujixerox.edmics-rlc"}, - { "rld", "application/resource-lists-diff+xml"}, - { "rm", "application/vnd.rn-realmedia"}, - { "rmi", "audio/midi"}, - { "rmp", "audio/x-pn-realaudio-plugin"}, - { "rms", "application/vnd.jcp.javame.midlet-rms"}, - { "rnc", "application/relax-ng-compact-syntax"}, - { "roff", "text/troff"}, - { "rpm", "application/x-rpm"}, - { "rpss", "application/vnd.nokia.radio-presets"}, - { "rpst", "application/vnd.nokia.radio-preset"}, - { "rq", "application/sparql-query"}, - { "rs", "application/rls-services+xml"}, - { "rsd", "application/rsd+xml"}, - { "rss", "application/rss+xml"}, - { "rtf", "application/rtf"}, - { "rtx", "text/richtext"}, - { "saf", "application/vnd.yamaha.smaf-audio"}, - { "sbml", "application/sbml+xml"}, - { "sc", "application/vnd.ibm.secure-container"}, - { "scd", "application/x-msschedule"}, - { "scm", "application/vnd.lotus-screencam"}, - { "scq", "application/scvp-cv-request"}, - { "scs", "application/scvp-cv-response"}, - { "sct", "text/scriptlet"}, - { "scurl", "text/vnd.curl.scurl"}, - { "sda", "application/vnd.stardivision.draw"}, - { "sdc", "application/vnd.stardivision.calc"}, - { "sdd", "application/vnd.stardivision.impress"}, - { "sdkd", "application/vnd.solent.sdkm+xml"}, - { "sdkm", "application/vnd.solent.sdkm+xml"}, - { "sdp", "application/sdp"}, - { "sdw", "application/vnd.stardivision.writer"}, - { "see", "application/vnd.seemail"}, - { "seed", "application/vnd.fdsn.seed"}, - { "sema", "application/vnd.sema"}, - { "semd", "application/vnd.semd"}, - { "semf", "application/vnd.semf"}, - { "ser", "application/java-serialized-object"}, - { "setpay", "application/set-payment-initiation"}, - { "setreg", "application/set-registration-initiation"}, - { "sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, - { "sfs", "application/vnd.spotfire.sfs"}, - { "sgl", "application/vnd.stardivision.writer-global"}, - { "sgml", "text/sgml"}, - { "sgm", "text/sgml"}, - { "sh", "application/x-sh"}, - { "shar", "application/x-shar"}, - { "shf", "application/shf+xml"}, - { "sic", "application/vnd.wap.sic"}, - { "sig", "application/pgp-signature"}, - { "silo", "model/mesh"}, - { "sis", "application/vnd.symbian.install"}, - { "sisx", "application/vnd.symbian.install"}, - { "sit", "application/x-stuffit"}, - { "si", "text/vnd.wap.si"}, - { "sitx", "application/x-stuffitx"}, - { "skd", "application/vnd.koan"}, - { "skm", "application/vnd.koan"}, - { "skp", "application/vnd.koan"}, - { "skt", "application/vnd.koan"}, - { "slc", "application/vnd.wap.slc"}, - { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, - { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, - { "slt", "application/vnd.epson.salt"}, - { "sl", "text/vnd.wap.sl"}, - { "smf", "application/vnd.stardivision.math"}, - { "smi", "application/smil+xml"}, - { "smil", "application/smil+xml"}, - { "snd", "audio/basic"}, - { "snf", "application/x-font-snf"}, - { "so", "application/octet-stream"}, - { "spc", "application/x-pkcs7-certificates"}, - { "spf", "application/vnd.yamaha.smaf-phrase"}, - { "spl", "application/x-futuresplash"}, - { "spot", "text/vnd.in3d.spot"}, - { "spp", "application/scvp-vp-response"}, - { "spq", "application/scvp-vp-request"}, - { "spx", "audio/ogg"}, - { "src", "application/x-wais-source"}, - { "srx", "application/sparql-results+xml"}, - { "sse", "application/vnd.kodak-descriptor"}, - { "ssf", "application/vnd.epson.ssf"}, - { "ssml", "application/ssml+xml"}, - { "sst", "application/vnd.ms-pkicertstore"}, - { "stc", "application/vnd.sun.xml.calc.template"}, - { "std", "application/vnd.sun.xml.draw.template"}, - { "s", "text/x-asm"}, - { "stf", "application/vnd.wt.stf"}, - { "sti", "application/vnd.sun.xml.impress.template"}, - { "stk", "application/hyperstudio"}, - { "stl", "application/vnd.ms-pki.stl"}, - { "stm", "text/html"}, - { "str", "application/vnd.pg.format"}, - { "stw", "application/vnd.sun.xml.writer.template"}, - { "sus", "application/vnd.sus-calendar"}, - { "susp", "application/vnd.sus-calendar"}, - { "sv4cpio", "application/x-sv4cpio"}, - { "sv4crc", "application/x-sv4crc"}, - { "svd", "application/vnd.svd"}, - { "svg", "image/svg+xml"}, - { "svgz", "image/svg+xml"}, - { "swa", "application/x-director"}, - { "swf", "application/x-shockwave-flash"}, - { "swi", "application/vnd.arastra.swi"}, - { "sxc", "application/vnd.sun.xml.calc"}, - { "sxd", "application/vnd.sun.xml.draw"}, - { "sxg", "application/vnd.sun.xml.writer.global"}, - { "sxi", "application/vnd.sun.xml.impress"}, - { "sxm", "application/vnd.sun.xml.math"}, - { "sxw", "application/vnd.sun.xml.writer"}, - { "tao", "application/vnd.tao.intent-module-archive"}, - { "t", "application/x-troff"}, - { "tar", "application/x-tar"}, - { "tcap", "application/vnd.3gpp2.tcap"}, - { "tcl", "application/x-tcl"}, - { "teacher", "application/vnd.smart.teacher"}, - { "tex", "application/x-tex"}, - { "texi", "application/x-texinfo"}, - { "texinfo", "application/x-texinfo"}, - { "text", "text/plain"}, - { "tfm", "application/x-tex-tfm"}, - { "tgz", "application/x-gzip"}, - { "tiff", "image/tiff"}, - { "tif", "image/tiff"}, - { "tmo", "application/vnd.tmobile-livetv"}, - { "torrent", "application/x-bittorrent"}, - { "tpl", "application/vnd.groove-tool-template"}, - { "tpt", "application/vnd.trid.tpt"}, - { "tra", "application/vnd.trueapp"}, - { "trm", "application/x-msterminal"}, - { "tr", "text/troff"}, - { "tsv", "text/tab-separated-values"}, - { "ttc", "application/x-font-ttf"}, - { "ttf", "application/x-font-ttf"}, - { "twd", "application/vnd.simtech-mindmapper"}, - { "twds", "application/vnd.simtech-mindmapper"}, - { "txd", "application/vnd.genomatix.tuxedo"}, - { "txf", "application/vnd.mobius.txf"}, - { "txt", "text/plain"}, - { "u32", "application/x-authorware-bin"}, - { "udeb", "application/x-debian-package"}, - { "ufd", "application/vnd.ufdl"}, - { "ufdl", "application/vnd.ufdl"}, - { "uls", "text/iuls"}, - { "umj", "application/vnd.umajin"}, - { "unityweb", "application/vnd.unity"}, - { "uoml", "application/vnd.uoml+xml"}, - { "uris", "text/uri-list"}, - { "uri", "text/uri-list"}, - { "urls", "text/uri-list"}, - { "ustar", "application/x-ustar"}, - { "utz", "application/vnd.uiq.theme"}, - { "uu", "text/x-uuencode"}, - { "vcd", "application/x-cdlink"}, - { "vcf", "text/x-vcard"}, - { "vcg", "application/vnd.groove-vcard"}, - { "vcs", "text/x-vcalendar"}, - { "vcx", "application/vnd.vcx"}, - { "vis", "application/vnd.visionary"}, - { "viv", "video/vnd.vivo"}, - { "vor", "application/vnd.stardivision.writer"}, - { "vox", "application/x-authorware-bin"}, - { "vrml", "x-world/x-vrml"}, - { "vsd", "application/vnd.visio"}, - { "vsf", "application/vnd.vsf"}, - { "vss", "application/vnd.visio"}, - { "vst", "application/vnd.visio"}, - { "vsw", "application/vnd.visio"}, - { "vtu", "model/vnd.vtu"}, - { "vxml", "application/voicexml+xml"}, - { "w3d", "application/x-director"}, - { "wad", "application/x-doom"}, - { "wav", "audio/x-wav"}, - { "wax", "audio/x-ms-wax"}, - { "wbmp", "image/vnd.wap.wbmp"}, - { "wbs", "application/vnd.criticaltools.wbs+xml"}, - { "wbxml", "application/vnd.wap.wbxml"}, - { "wcm", "application/vnd.ms-works"}, - { "wdb", "application/vnd.ms-works"}, - { "wiz", "application/msword"}, - { "wks", "application/vnd.ms-works"}, - { "wma", "audio/x-ms-wma"}, - { "wmd", "application/x-ms-wmd"}, - { "wmf", "application/x-msmetafile"}, - { "wmlc", "application/vnd.wap.wmlc"}, - { "wmlsc", "application/vnd.wap.wmlscriptc"}, - { "wmls", "text/vnd.wap.wmlscript"}, - { "wml", "text/vnd.wap.wml"}, - { "wm", "video/x-ms-wm"}, - { "wmv", "video/x-ms-wmv"}, - { "wmx", "video/x-ms-wmx"}, - { "wmz", "application/x-ms-wmz"}, - { "wpd", "application/vnd.wordperfect"}, - { "wpl", "application/vnd.ms-wpl"}, - { "wps", "application/vnd.ms-works"}, - { "wqd", "application/vnd.wqd"}, - { "wri", "application/x-mswrite"}, - { "wrl", "x-world/x-vrml"}, - { "wrz", "x-world/x-vrml"}, - { "wsdl", "application/wsdl+xml"}, - { "wspolicy", "application/wspolicy+xml"}, - { "wtb", "application/vnd.webturbo"}, - { "wvx", "video/x-ms-wvx"}, - { "x32", "application/x-authorware-bin"}, - { "x3d", "application/vnd.hzn-3d-crossword"}, - { "xaf", "x-world/x-vrml"}, - { "xap", "application/x-silverlight-app"}, - { "xar", "application/vnd.xara"}, - { "xbap", "application/x-ms-xbap"}, - { "xbd", "application/vnd.fujixerox.docuworks.binder"}, - { "xbm", "image/x-xbitmap"}, - { "xdm", "application/vnd.syncml.dm+xml"}, - { "xdp", "application/vnd.adobe.xdp+xml"}, - { "xdw", "application/vnd.fujixerox.docuworks"}, - { "xenc", "application/xenc+xml"}, - { "xer", "application/patch-ops-error+xml"}, - { "xfdf", "application/vnd.adobe.xfdf"}, - { "xfdl", "application/vnd.xfdl"}, - { "xht", "application/xhtml+xml"}, - { "xhtml", "application/xhtml+xml"}, - { "xhvml", "application/xv+xml"}, - { "xif", "image/vnd.xiff"}, - { "xla", "application/vnd.ms-excel"}, - { "xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, - { "xlb", "application/vnd.ms-excel"}, - { "xlc", "application/vnd.ms-excel"}, - { "xlm", "application/vnd.ms-excel"}, - { "xls", "application/vnd.ms-excel"}, - { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, - { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, - { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - { "xlt", "application/vnd.ms-excel"}, - { "xltm", "application/vnd.ms-excel.template.macroenabled.12"}, - { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, - { "xlw", "application/vnd.ms-excel"}, - { "xml", "application/xml"}, - { "xo", "application/vnd.olpc-sugar"}, - { "xof", "x-world/x-vrml"}, - { "xop", "application/xop+xml"}, - { "xpdl", "application/xml"}, - { "xpi", "application/x-xpinstall"}, - { "xpm", "image/x-xpixmap"}, - { "xpr", "application/vnd.is-xpr"}, - { "xps", "application/vnd.ms-xpsdocument"}, - { "xpw", "application/vnd.intercon.formnet"}, - { "xpx", "application/vnd.intercon.formnet"}, - { "xsl", "application/xml"}, - { "xslt", "application/xslt+xml"}, - { "xsm", "application/vnd.syncml+xml"}, - { "xspf", "application/xspf+xml"}, - { "xul", "application/vnd.mozilla.xul+xml"}, - { "xvm", "application/xv+xml"}, - { "xvml", "application/xv+xml"}, - { "xwd", "image/x-xwindowdump"}, - { "xyz", "chemical/x-xyz"}, - { "z", "application/x-compress"}, - { "zaz", "application/vnd.zzazz.deck+xml"}, - { "zip", "application/zip"}, - { "zir", "application/vnd.zul"}, - { "zirz", "application/vnd.zul"}, - { "zmm", "application/vnd.handheld-entertainment+xml"} + //* Do not delete the next two lines + { "", DEFAULT_ATTACHMENT_MIME_TYPE }, + { "k9s", K9_SETTINGS_MIME_TYPE}, + //* Do not delete the previous two lines + { "123", "application/vnd.lotus-1-2-3"}, + { "323", "text/h323"}, + { "3dml", "text/vnd.in3d.3dml"}, + { "3g2", "video/3gpp2"}, + { "3gp", "video/3gpp"}, + { "aab", "application/x-authorware-bin"}, + { "aac", "audio/x-aac"}, + { "aam", "application/x-authorware-map"}, + { "a", "application/octet-stream"}, + { "aas", "application/x-authorware-seg"}, + { "abw", "application/x-abiword"}, + { "acc", "application/vnd.americandynamics.acc"}, + { "ace", "application/x-ace-compressed"}, + { "acu", "application/vnd.acucobol"}, + { "acutc", "application/vnd.acucorp"}, + { "acx", "application/internet-property-stream"}, + { "adp", "audio/adpcm"}, + { "aep", "application/vnd.audiograph"}, + { "afm", "application/x-font-type1"}, + { "afp", "application/vnd.ibm.modcap"}, + { "ai", "application/postscript"}, + { "aif", "audio/x-aiff"}, + { "aifc", "audio/x-aiff"}, + { "aiff", "audio/x-aiff"}, + { "air", "application/vnd.adobe.air-application-installer-package+zip"}, + { "ami", "application/vnd.amiga.ami"}, + { "apk", "application/vnd.android.package-archive"}, + { "application", "application/x-ms-application"}, + { "apr", "application/vnd.lotus-approach"}, + { "asc", "application/pgp-signature"}, + { "asf", "video/x-ms-asf"}, + { "asm", "text/x-asm"}, + { "aso", "application/vnd.accpac.simply.aso"}, + { "asr", "video/x-ms-asf"}, + { "asx", "video/x-ms-asf"}, + { "atc", "application/vnd.acucorp"}, + { "atom", "application/atom+xml"}, + { "atomcat", "application/atomcat+xml"}, + { "atomsvc", "application/atomsvc+xml"}, + { "atx", "application/vnd.antix.game-component"}, + { "au", "audio/basic"}, + { "avi", "video/x-msvideo"}, + { "aw", "application/applixware"}, + { "axs", "application/olescript"}, + { "azf", "application/vnd.airzip.filesecure.azf"}, + { "azs", "application/vnd.airzip.filesecure.azs"}, + { "azw", "application/vnd.amazon.ebook"}, + { "bas", "text/plain"}, + { "bat", "application/x-msdownload"}, + { "bcpio", "application/x-bcpio"}, + { "bdf", "application/x-font-bdf"}, + { "bdm", "application/vnd.syncml.dm+wbxml"}, + { "bh2", "application/vnd.fujitsu.oasysprs"}, + { "bin", "application/octet-stream"}, + { "bmi", "application/vnd.bmi"}, + { "bmp", "image/bmp"}, + { "book", "application/vnd.framemaker"}, + { "box", "application/vnd.previewsystems.box"}, + { "boz", "application/x-bzip2"}, + { "bpk", "application/octet-stream"}, + { "btif", "image/prs.btif"}, + { "bz2", "application/x-bzip2"}, + { "bz", "application/x-bzip"}, + { "c4d", "application/vnd.clonk.c4group"}, + { "c4f", "application/vnd.clonk.c4group"}, + { "c4g", "application/vnd.clonk.c4group"}, + { "c4p", "application/vnd.clonk.c4group"}, + { "c4u", "application/vnd.clonk.c4group"}, + { "cab", "application/vnd.ms-cab-compressed"}, + { "car", "application/vnd.curl.car"}, + { "cat", "application/vnd.ms-pki.seccat"}, + { "cct", "application/x-director"}, + { "cc", "text/x-c"}, + { "ccxml", "application/ccxml+xml"}, + { "cdbcmsg", "application/vnd.contact.cmsg"}, + { "cdf", "application/x-cdf"}, + { "cdkey", "application/vnd.mediastation.cdkey"}, + { "cdx", "chemical/x-cdx"}, + { "cdxml", "application/vnd.chemdraw+xml"}, + { "cdy", "application/vnd.cinderella"}, + { "cer", "application/x-x509-ca-cert"}, + { "cgm", "image/cgm"}, + { "chat", "application/x-chat"}, + { "chm", "application/vnd.ms-htmlhelp"}, + { "chrt", "application/vnd.kde.kchart"}, + { "cif", "chemical/x-cif"}, + { "cii", "application/vnd.anser-web-certificate-issue-initiation"}, + { "cla", "application/vnd.claymore"}, + { "class", "application/java-vm"}, + { "clkk", "application/vnd.crick.clicker.keyboard"}, + { "clkp", "application/vnd.crick.clicker.palette"}, + { "clkt", "application/vnd.crick.clicker.template"}, + { "clkw", "application/vnd.crick.clicker.wordbank"}, + { "clkx", "application/vnd.crick.clicker"}, + { "clp", "application/x-msclip"}, + { "cmc", "application/vnd.cosmocaller"}, + { "cmdf", "chemical/x-cmdf"}, + { "cml", "chemical/x-cml"}, + { "cmp", "application/vnd.yellowriver-custom-menu"}, + { "cmx", "image/x-cmx"}, + { "cod", "application/vnd.rim.cod"}, + { "com", "application/x-msdownload"}, + { "conf", "text/plain"}, + { "cpio", "application/x-cpio"}, + { "cpp", "text/x-c"}, + { "cpt", "application/mac-compactpro"}, + { "crd", "application/x-mscardfile"}, + { "crl", "application/pkix-crl"}, + { "crt", "application/x-x509-ca-cert"}, + { "csh", "application/x-csh"}, + { "csml", "chemical/x-csml"}, + { "csp", "application/vnd.commonspace"}, + { "css", "text/css"}, + { "cst", "application/x-director"}, + { "csv", "text/csv"}, + { "c", "text/plain"}, + { "cu", "application/cu-seeme"}, + { "curl", "text/vnd.curl"}, + { "cww", "application/prs.cww"}, + { "cxt", "application/x-director"}, + { "cxx", "text/x-c"}, + { "daf", "application/vnd.mobius.daf"}, + { "dataless", "application/vnd.fdsn.seed"}, + { "davmount", "application/davmount+xml"}, + { "dcr", "application/x-director"}, + { "dcurl", "text/vnd.curl.dcurl"}, + { "dd2", "application/vnd.oma.dd2+xml"}, + { "ddd", "application/vnd.fujixerox.ddd"}, + { "deb", "application/x-debian-package"}, + { "def", "text/plain"}, + { "deploy", "application/octet-stream"}, + { "der", "application/x-x509-ca-cert"}, + { "dfac", "application/vnd.dreamfactory"}, + { "dic", "text/x-c"}, + { "diff", "text/plain"}, + { "dir", "application/x-director"}, + { "dis", "application/vnd.mobius.dis"}, + { "dist", "application/octet-stream"}, + { "distz", "application/octet-stream"}, + { "djv", "image/vnd.djvu"}, + { "djvu", "image/vnd.djvu"}, + { "dll", "application/x-msdownload"}, + { "dmg", "application/octet-stream"}, + { "dms", "application/octet-stream"}, + { "dna", "application/vnd.dna"}, + { "doc", "application/msword"}, + { "docm", "application/vnd.ms-word.document.macroenabled.12"}, + { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + { "dot", "application/msword"}, + { "dotm", "application/vnd.ms-word.template.macroenabled.12"}, + { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + { "dp", "application/vnd.osgi.dp"}, + { "dpg", "application/vnd.dpgraph"}, + { "dsc", "text/prs.lines.tag"}, + { "dtb", "application/x-dtbook+xml"}, + { "dtd", "application/xml-dtd"}, + { "dts", "audio/vnd.dts"}, + { "dtshd", "audio/vnd.dts.hd"}, + { "dump", "application/octet-stream"}, + { "dvi", "application/x-dvi"}, + { "dwf", "model/vnd.dwf"}, + { "dwg", "image/vnd.dwg"}, + { "dxf", "image/vnd.dxf"}, + { "dxp", "application/vnd.spotfire.dxp"}, + { "dxr", "application/x-director"}, + { "ecelp4800", "audio/vnd.nuera.ecelp4800"}, + { "ecelp7470", "audio/vnd.nuera.ecelp7470"}, + { "ecelp9600", "audio/vnd.nuera.ecelp9600"}, + { "ecma", "application/ecmascript"}, + { "edm", "application/vnd.novadigm.edm"}, + { "edx", "application/vnd.novadigm.edx"}, + { "efif", "application/vnd.picsel"}, + { "ei6", "application/vnd.pg.osasli"}, + { "elc", "application/octet-stream"}, + { "eml", "message/rfc822"}, + { "emma", "application/emma+xml"}, + { "eol", "audio/vnd.digital-winds"}, + { "eot", "application/vnd.ms-fontobject"}, + { "eps", "application/postscript"}, + { "epub", "application/epub+zip"}, + { "es3", "application/vnd.eszigno3+xml"}, + { "esf", "application/vnd.epson.esf"}, + { "et3", "application/vnd.eszigno3+xml"}, + { "etx", "text/x-setext"}, + { "evy", "application/envoy"}, + { "exe", "application/octet-stream"}, + { "ext", "application/vnd.novadigm.ext"}, + { "ez2", "application/vnd.ezpix-album"}, + { "ez3", "application/vnd.ezpix-package"}, + { "ez", "application/andrew-inset"}, + { "f4v", "video/x-f4v"}, + { "f77", "text/x-fortran"}, + { "f90", "text/x-fortran"}, + { "fbs", "image/vnd.fastbidsheet"}, + { "fdf", "application/vnd.fdf"}, + { "fe_launch", "application/vnd.denovo.fcselayout-link"}, + { "fg5", "application/vnd.fujitsu.oasysgp"}, + { "fgd", "application/x-director"}, + { "fh4", "image/x-freehand"}, + { "fh5", "image/x-freehand"}, + { "fh7", "image/x-freehand"}, + { "fhc", "image/x-freehand"}, + { "fh", "image/x-freehand"}, + { "fif", "application/fractals"}, + { "fig", "application/x-xfig"}, + { "fli", "video/x-fli"}, + { "flo", "application/vnd.micrografx.flo"}, + { "flr", "x-world/x-vrml"}, + { "flv", "video/x-flv"}, + { "flw", "application/vnd.kde.kivio"}, + { "flx", "text/vnd.fmi.flexstor"}, + { "fly", "text/vnd.fly"}, + { "fm", "application/vnd.framemaker"}, + { "fnc", "application/vnd.frogans.fnc"}, + { "for", "text/x-fortran"}, + { "fpx", "image/vnd.fpx"}, + { "frame", "application/vnd.framemaker"}, + { "fsc", "application/vnd.fsc.weblaunch"}, + { "fst", "image/vnd.fst"}, + { "ftc", "application/vnd.fluxtime.clip"}, + { "f", "text/x-fortran"}, + { "fti", "application/vnd.anser-web-funds-transfer-initiation"}, + { "fvt", "video/vnd.fvt"}, + { "fzs", "application/vnd.fuzzysheet"}, + { "g3", "image/g3fax"}, + { "gac", "application/vnd.groove-account"}, + { "gdl", "model/vnd.gdl"}, + { "geo", "application/vnd.dynageo"}, + { "gex", "application/vnd.geometry-explorer"}, + { "ggb", "application/vnd.geogebra.file"}, + { "ggt", "application/vnd.geogebra.tool"}, + { "ghf", "application/vnd.groove-help"}, + { "gif", "image/gif"}, + { "gim", "application/vnd.groove-identity-message"}, + { "gmx", "application/vnd.gmx"}, + { "gnumeric", "application/x-gnumeric"}, + { "gph", "application/vnd.flographit"}, + { "gqf", "application/vnd.grafeq"}, + { "gqs", "application/vnd.grafeq"}, + { "gram", "application/srgs"}, + { "gre", "application/vnd.geometry-explorer"}, + { "grv", "application/vnd.groove-injector"}, + { "grxml", "application/srgs+xml"}, + { "gsf", "application/x-font-ghostscript"}, + { "gtar", "application/x-gtar"}, + { "gtm", "application/vnd.groove-tool-message"}, + { "gtw", "model/vnd.gtw"}, + { "gv", "text/vnd.graphviz"}, + { "gz", "application/x-gzip"}, + { "h261", "video/h261"}, + { "h263", "video/h263"}, + { "h264", "video/h264"}, + { "hbci", "application/vnd.hbci"}, + { "hdf", "application/x-hdf"}, + { "hh", "text/x-c"}, + { "hlp", "application/winhlp"}, + { "hpgl", "application/vnd.hp-hpgl"}, + { "hpid", "application/vnd.hp-hpid"}, + { "hps", "application/vnd.hp-hps"}, + { "hqx", "application/mac-binhex40"}, + { "hta", "application/hta"}, + { "htc", "text/x-component"}, + { "h", "text/plain"}, + { "htke", "application/vnd.kenameaapp"}, + { "html", "text/html"}, + { "htm", "text/html"}, + { "htt", "text/webviewhtml"}, + { "hvd", "application/vnd.yamaha.hv-dic"}, + { "hvp", "application/vnd.yamaha.hv-voice"}, + { "hvs", "application/vnd.yamaha.hv-script"}, + { "icc", "application/vnd.iccprofile"}, + { "ice", "x-conference/x-cooltalk"}, + { "icm", "application/vnd.iccprofile"}, + { "ico", "image/x-icon"}, + { "ics", "text/calendar"}, + { "ief", "image/ief"}, + { "ifb", "text/calendar"}, + { "ifm", "application/vnd.shana.informed.formdata"}, + { "iges", "model/iges"}, + { "igl", "application/vnd.igloader"}, + { "igs", "model/iges"}, + { "igx", "application/vnd.micrografx.igx"}, + { "iif", "application/vnd.shana.informed.interchange"}, + { "iii", "application/x-iphone"}, + { "imp", "application/vnd.accpac.simply.imp"}, + { "ims", "application/vnd.ms-ims"}, + { "ins", "application/x-internet-signup"}, + { "in", "text/plain"}, + { "ipk", "application/vnd.shana.informed.package"}, + { "irm", "application/vnd.ibm.rights-management"}, + { "irp", "application/vnd.irepository.package+xml"}, + { "iso", "application/octet-stream"}, + { "isp", "application/x-internet-signup"}, + { "itp", "application/vnd.shana.informed.formtemplate"}, + { "ivp", "application/vnd.immervision-ivp"}, + { "ivu", "application/vnd.immervision-ivu"}, + { "jad", "text/vnd.sun.j2me.app-descriptor"}, + { "jam", "application/vnd.jam"}, + { "jar", "application/java-archive"}, + { "java", "text/x-java-source"}, + { "jfif", "image/pipeg"}, + { "jisp", "application/vnd.jisp"}, + { "jlt", "application/vnd.hp-jlyt"}, + { "jnlp", "application/x-java-jnlp-file"}, + { "joda", "application/vnd.joost.joda-archive"}, + { "jpeg", "image/jpeg"}, + { "jpe", "image/jpeg"}, + { "jpg", "image/jpeg"}, + { "jpgm", "video/jpm"}, + { "jpgv", "video/jpeg"}, + { "jpm", "video/jpm"}, + { "js", "application/x-javascript"}, + { "json", "application/json"}, + { "kar", "audio/midi"}, + { "karbon", "application/vnd.kde.karbon"}, + { "kfo", "application/vnd.kde.kformula"}, + { "kia", "application/vnd.kidspiration"}, + { "kil", "application/x-killustrator"}, + { "kml", "application/vnd.google-earth.kml+xml"}, + { "kmz", "application/vnd.google-earth.kmz"}, + { "kne", "application/vnd.kinar"}, + { "knp", "application/vnd.kinar"}, + { "kon", "application/vnd.kde.kontour"}, + { "kpr", "application/vnd.kde.kpresenter"}, + { "kpt", "application/vnd.kde.kpresenter"}, + { "ksh", "text/plain"}, + { "ksp", "application/vnd.kde.kspread"}, + { "ktr", "application/vnd.kahootz"}, + { "ktz", "application/vnd.kahootz"}, + { "kwd", "application/vnd.kde.kword"}, + { "kwt", "application/vnd.kde.kword"}, + { "latex", "application/x-latex"}, + { "lbd", "application/vnd.llamagraphics.life-balance.desktop"}, + { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, + { "les", "application/vnd.hhe.lesson-player"}, + { "lha", "application/octet-stream"}, + { "link66", "application/vnd.route66.link66+xml"}, + { "list3820", "application/vnd.ibm.modcap"}, + { "listafp", "application/vnd.ibm.modcap"}, + { "list", "text/plain"}, + { "log", "text/plain"}, + { "lostxml", "application/lost+xml"}, + { "lrf", "application/octet-stream"}, + { "lrm", "application/vnd.ms-lrm"}, + { "lsf", "video/x-la-asf"}, + { "lsx", "video/x-la-asf"}, + { "ltf", "application/vnd.frogans.ltf"}, + { "lvp", "audio/vnd.lucent.voice"}, + { "lwp", "application/vnd.lotus-wordpro"}, + { "lzh", "application/octet-stream"}, + { "m13", "application/x-msmediaview"}, + { "m14", "application/x-msmediaview"}, + { "m1v", "video/mpeg"}, + { "m2a", "audio/mpeg"}, + { "m2v", "video/mpeg"}, + { "m3a", "audio/mpeg"}, + { "m3u", "audio/x-mpegurl"}, + { "m4u", "video/vnd.mpegurl"}, + { "m4v", "video/x-m4v"}, + { "ma", "application/mathematica"}, + { "mag", "application/vnd.ecowin.chart"}, + { "maker", "application/vnd.framemaker"}, + { "man", "text/troff"}, + { "mathml", "application/mathml+xml"}, + { "mb", "application/mathematica"}, + { "mbk", "application/vnd.mobius.mbk"}, + { "mbox", "application/mbox"}, + { "mc1", "application/vnd.medcalcdata"}, + { "mcd", "application/vnd.mcd"}, + { "mcurl", "text/vnd.curl.mcurl"}, + { "mdb", "application/x-msaccess"}, + { "mdi", "image/vnd.ms-modi"}, + { "mesh", "model/mesh"}, + { "me", "text/troff"}, + { "mfm", "application/vnd.mfmp"}, + { "mgz", "application/vnd.proteus.magazine"}, + { "mht", "message/rfc822"}, + { "mhtml", "message/rfc822"}, + { "mid", "audio/midi"}, + { "midi", "audio/midi"}, + { "mif", "application/vnd.mif"}, + { "mime", "message/rfc822"}, + { "mj2", "video/mj2"}, + { "mjp2", "video/mj2"}, + { "mlp", "application/vnd.dolby.mlp"}, + { "mmd", "application/vnd.chipnuts.karaoke-mmd"}, + { "mmf", "application/vnd.smaf"}, + { "mmr", "image/vnd.fujixerox.edmics-mmr"}, + { "mny", "application/x-msmoney"}, + { "mobi", "application/x-mobipocket-ebook"}, + { "movie", "video/x-sgi-movie"}, + { "mov", "video/quicktime"}, + { "mp2a", "audio/mpeg"}, + { "mp2", "video/mpeg"}, + { "mp3", "audio/mpeg"}, + { "mp4a", "audio/mp4"}, + { "mp4s", "application/mp4"}, + { "mp4", "video/mp4"}, + { "mp4v", "video/mp4"}, + { "mpa", "video/mpeg"}, + { "mpc", "application/vnd.mophun.certificate"}, + { "mpeg", "video/mpeg"}, + { "mpe", "video/mpeg"}, + { "mpg4", "video/mp4"}, + { "mpga", "audio/mpeg"}, + { "mpg", "video/mpeg"}, + { "mpkg", "application/vnd.apple.installer+xml"}, + { "mpm", "application/vnd.blueice.multipass"}, + { "mpn", "application/vnd.mophun.application"}, + { "mpp", "application/vnd.ms-project"}, + { "mpt", "application/vnd.ms-project"}, + { "mpv2", "video/mpeg"}, + { "mpy", "application/vnd.ibm.minipay"}, + { "mqy", "application/vnd.mobius.mqy"}, + { "mrc", "application/marc"}, + { "mscml", "application/mediaservercontrol+xml"}, + { "mseed", "application/vnd.fdsn.mseed"}, + { "mseq", "application/vnd.mseq"}, + { "msf", "application/vnd.epson.msf"}, + { "msh", "model/mesh"}, + { "msi", "application/x-msdownload"}, + { "ms", "text/troff"}, + { "msty", "application/vnd.muvee.style"}, + { "mts", "model/vnd.mts"}, + { "mus", "application/vnd.musician"}, + { "musicxml", "application/vnd.recordare.musicxml+xml"}, + { "mvb", "application/x-msmediaview"}, + { "mxf", "application/mxf"}, + { "mxl", "application/vnd.recordare.musicxml"}, + { "mxml", "application/xv+xml"}, + { "mxs", "application/vnd.triscape.mxs"}, + { "mxu", "video/vnd.mpegurl"}, + { "nb", "application/mathematica"}, + { "nc", "application/x-netcdf"}, + { "ncx", "application/x-dtbncx+xml"}, + { "n-gage", "application/vnd.nokia.n-gage.symbian.install"}, + { "ngdat", "application/vnd.nokia.n-gage.data"}, + { "nlu", "application/vnd.neurolanguage.nlu"}, + { "nml", "application/vnd.enliven"}, + { "nnd", "application/vnd.noblenet-directory"}, + { "nns", "application/vnd.noblenet-sealer"}, + { "nnw", "application/vnd.noblenet-web"}, + { "npx", "image/vnd.net-fpx"}, + { "nsf", "application/vnd.lotus-notes"}, + { "nws", "message/rfc822"}, + { "oa2", "application/vnd.fujitsu.oasys2"}, + { "oa3", "application/vnd.fujitsu.oasys3"}, + { "o", "application/octet-stream"}, + { "oas", "application/vnd.fujitsu.oasys"}, + { "obd", "application/x-msbinder"}, + { "obj", "application/octet-stream"}, + { "oda", "application/oda"}, + { "odb", "application/vnd.oasis.opendocument.database"}, + { "odc", "application/vnd.oasis.opendocument.chart"}, + { "odf", "application/vnd.oasis.opendocument.formula"}, + { "odft", "application/vnd.oasis.opendocument.formula-template"}, + { "odg", "application/vnd.oasis.opendocument.graphics"}, + { "odi", "application/vnd.oasis.opendocument.image"}, + { "odp", "application/vnd.oasis.opendocument.presentation"}, + { "ods", "application/vnd.oasis.opendocument.spreadsheet"}, + { "odt", "application/vnd.oasis.opendocument.text"}, + { "oga", "audio/ogg"}, + { "ogg", "audio/ogg"}, + { "ogv", "video/ogg"}, + { "ogx", "application/ogg"}, + { "onepkg", "application/onenote"}, + { "onetmp", "application/onenote"}, + { "onetoc2", "application/onenote"}, + { "onetoc", "application/onenote"}, + { "opf", "application/oebps-package+xml"}, + { "oprc", "application/vnd.palm"}, + { "org", "application/vnd.lotus-organizer"}, + { "osf", "application/vnd.yamaha.openscoreformat"}, + { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, + { "otc", "application/vnd.oasis.opendocument.chart-template"}, + { "otf", "application/x-font-otf"}, + { "otg", "application/vnd.oasis.opendocument.graphics-template"}, + { "oth", "application/vnd.oasis.opendocument.text-web"}, + { "oti", "application/vnd.oasis.opendocument.image-template"}, + { "otm", "application/vnd.oasis.opendocument.text-master"}, + { "otp", "application/vnd.oasis.opendocument.presentation-template"}, + { "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + { "ott", "application/vnd.oasis.opendocument.text-template"}, + { "oxt", "application/vnd.openofficeorg.extension"}, + { "p10", "application/pkcs10"}, + { "p12", "application/x-pkcs12"}, + { "p7b", "application/x-pkcs7-certificates"}, + { "p7c", "application/x-pkcs7-mime"}, + { "p7m", "application/x-pkcs7-mime"}, + { "p7r", "application/x-pkcs7-certreqresp"}, + { "p7s", "application/x-pkcs7-signature"}, + { "pas", "text/x-pascal"}, + { "pbd", "application/vnd.powerbuilder6"}, + { "pbm", "image/x-portable-bitmap"}, + { "pcf", "application/x-font-pcf"}, + { "pcl", "application/vnd.hp-pcl"}, + { "pclxl", "application/vnd.hp-pclxl"}, + { "pct", "image/x-pict"}, + { "pcurl", "application/vnd.curl.pcurl"}, + { "pcx", "image/x-pcx"}, + { "pdb", "application/vnd.palm"}, + { "pdf", "application/pdf"}, + { "pfa", "application/x-font-type1"}, + { "pfb", "application/x-font-type1"}, + { "pfm", "application/x-font-type1"}, + { "pfr", "application/font-tdpfr"}, + { "pfx", "application/x-pkcs12"}, + { "pgm", "image/x-portable-graymap"}, + { "pgn", "application/x-chess-pgn"}, + { "pgp", "application/pgp-encrypted"}, + { "pic", "image/x-pict"}, + { "pkg", "application/octet-stream"}, + { "pki", "application/pkixcmp"}, + { "pkipath", "application/pkix-pkipath"}, + { "pko", "application/ynd.ms-pkipko"}, + { "plb", "application/vnd.3gpp.pic-bw-large"}, + { "plc", "application/vnd.mobius.plc"}, + { "plf", "application/vnd.pocketlearn"}, + { "pls", "application/pls+xml"}, + { "pl", "text/plain"}, + { "pma", "application/x-perfmon"}, + { "pmc", "application/x-perfmon"}, + { "pml", "application/x-perfmon"}, + { "pmr", "application/x-perfmon"}, + { "pmw", "application/x-perfmon"}, + { "png", "image/png"}, + { "pnm", "image/x-portable-anymap"}, + { "portpkg", "application/vnd.macports.portpkg"}, + { "pot,", "application/vnd.ms-powerpoint"}, + { "pot", "application/vnd.ms-powerpoint"}, + { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, + { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + { "ppa", "application/vnd.ms-powerpoint"}, + { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, + { "ppd", "application/vnd.cups-ppd"}, + { "ppm", "image/x-portable-pixmap"}, + { "pps", "application/vnd.ms-powerpoint"}, + { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, + { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + { "ppt", "application/vnd.ms-powerpoint"}, + { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, + { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + { "pqa", "application/vnd.palm"}, + { "prc", "application/x-mobipocket-ebook"}, + { "pre", "application/vnd.lotus-freelance"}, + { "prf", "application/pics-rules"}, + { "ps", "application/postscript"}, + { "psb", "application/vnd.3gpp.pic-bw-small"}, + { "psd", "image/vnd.adobe.photoshop"}, + { "psf", "application/x-font-linux-psf"}, + { "p", "text/x-pascal"}, + { "ptid", "application/vnd.pvi.ptid1"}, + { "pub", "application/x-mspublisher"}, + { "pvb", "application/vnd.3gpp.pic-bw-var"}, + { "pwn", "application/vnd.3m.post-it-notes"}, + { "pwz", "application/vnd.ms-powerpoint"}, + { "pya", "audio/vnd.ms-playready.media.pya"}, + { "pyc", "application/x-python-code"}, + { "pyo", "application/x-python-code"}, + { "py", "text/x-python"}, + { "pyv", "video/vnd.ms-playready.media.pyv"}, + { "qam", "application/vnd.epson.quickanime"}, + { "qbo", "application/vnd.intu.qbo"}, + { "qfx", "application/vnd.intu.qfx"}, + { "qps", "application/vnd.publishare-delta-tree"}, + { "qt", "video/quicktime"}, + { "qwd", "application/vnd.quark.quarkxpress"}, + { "qwt", "application/vnd.quark.quarkxpress"}, + { "qxb", "application/vnd.quark.quarkxpress"}, + { "qxd", "application/vnd.quark.quarkxpress"}, + { "qxl", "application/vnd.quark.quarkxpress"}, + { "qxt", "application/vnd.quark.quarkxpress"}, + { "ra", "audio/x-pn-realaudio"}, + { "ram", "audio/x-pn-realaudio"}, + { "rar", "application/x-rar-compressed"}, + { "ras", "image/x-cmu-raster"}, + { "rcprofile", "application/vnd.ipunplugged.rcprofile"}, + { "rdf", "application/rdf+xml"}, + { "rdz", "application/vnd.data-vision.rdz"}, + { "rep", "application/vnd.businessobjects"}, + { "res", "application/x-dtbresource+xml"}, + { "rgb", "image/x-rgb"}, + { "rif", "application/reginfo+xml"}, + { "rl", "application/resource-lists+xml"}, + { "rlc", "image/vnd.fujixerox.edmics-rlc"}, + { "rld", "application/resource-lists-diff+xml"}, + { "rm", "application/vnd.rn-realmedia"}, + { "rmi", "audio/midi"}, + { "rmp", "audio/x-pn-realaudio-plugin"}, + { "rms", "application/vnd.jcp.javame.midlet-rms"}, + { "rnc", "application/relax-ng-compact-syntax"}, + { "roff", "text/troff"}, + { "rpm", "application/x-rpm"}, + { "rpss", "application/vnd.nokia.radio-presets"}, + { "rpst", "application/vnd.nokia.radio-preset"}, + { "rq", "application/sparql-query"}, + { "rs", "application/rls-services+xml"}, + { "rsd", "application/rsd+xml"}, + { "rss", "application/rss+xml"}, + { "rtf", "application/rtf"}, + { "rtx", "text/richtext"}, + { "saf", "application/vnd.yamaha.smaf-audio"}, + { "sbml", "application/sbml+xml"}, + { "sc", "application/vnd.ibm.secure-container"}, + { "scd", "application/x-msschedule"}, + { "scm", "application/vnd.lotus-screencam"}, + { "scq", "application/scvp-cv-request"}, + { "scs", "application/scvp-cv-response"}, + { "sct", "text/scriptlet"}, + { "scurl", "text/vnd.curl.scurl"}, + { "sda", "application/vnd.stardivision.draw"}, + { "sdc", "application/vnd.stardivision.calc"}, + { "sdd", "application/vnd.stardivision.impress"}, + { "sdkd", "application/vnd.solent.sdkm+xml"}, + { "sdkm", "application/vnd.solent.sdkm+xml"}, + { "sdp", "application/sdp"}, + { "sdw", "application/vnd.stardivision.writer"}, + { "see", "application/vnd.seemail"}, + { "seed", "application/vnd.fdsn.seed"}, + { "sema", "application/vnd.sema"}, + { "semd", "application/vnd.semd"}, + { "semf", "application/vnd.semf"}, + { "ser", "application/java-serialized-object"}, + { "setpay", "application/set-payment-initiation"}, + { "setreg", "application/set-registration-initiation"}, + { "sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, + { "sfs", "application/vnd.spotfire.sfs"}, + { "sgl", "application/vnd.stardivision.writer-global"}, + { "sgml", "text/sgml"}, + { "sgm", "text/sgml"}, + { "sh", "application/x-sh"}, + { "shar", "application/x-shar"}, + { "shf", "application/shf+xml"}, + { "sic", "application/vnd.wap.sic"}, + { "sig", "application/pgp-signature"}, + { "silo", "model/mesh"}, + { "sis", "application/vnd.symbian.install"}, + { "sisx", "application/vnd.symbian.install"}, + { "sit", "application/x-stuffit"}, + { "si", "text/vnd.wap.si"}, + { "sitx", "application/x-stuffitx"}, + { "skd", "application/vnd.koan"}, + { "skm", "application/vnd.koan"}, + { "skp", "application/vnd.koan"}, + { "skt", "application/vnd.koan"}, + { "slc", "application/vnd.wap.slc"}, + { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, + { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + { "slt", "application/vnd.epson.salt"}, + { "sl", "text/vnd.wap.sl"}, + { "smf", "application/vnd.stardivision.math"}, + { "smi", "application/smil+xml"}, + { "smil", "application/smil+xml"}, + { "snd", "audio/basic"}, + { "snf", "application/x-font-snf"}, + { "so", "application/octet-stream"}, + { "spc", "application/x-pkcs7-certificates"}, + { "spf", "application/vnd.yamaha.smaf-phrase"}, + { "spl", "application/x-futuresplash"}, + { "spot", "text/vnd.in3d.spot"}, + { "spp", "application/scvp-vp-response"}, + { "spq", "application/scvp-vp-request"}, + { "spx", "audio/ogg"}, + { "src", "application/x-wais-source"}, + { "srx", "application/sparql-results+xml"}, + { "sse", "application/vnd.kodak-descriptor"}, + { "ssf", "application/vnd.epson.ssf"}, + { "ssml", "application/ssml+xml"}, + { "sst", "application/vnd.ms-pkicertstore"}, + { "stc", "application/vnd.sun.xml.calc.template"}, + { "std", "application/vnd.sun.xml.draw.template"}, + { "s", "text/x-asm"}, + { "stf", "application/vnd.wt.stf"}, + { "sti", "application/vnd.sun.xml.impress.template"}, + { "stk", "application/hyperstudio"}, + { "stl", "application/vnd.ms-pki.stl"}, + { "stm", "text/html"}, + { "str", "application/vnd.pg.format"}, + { "stw", "application/vnd.sun.xml.writer.template"}, + { "sus", "application/vnd.sus-calendar"}, + { "susp", "application/vnd.sus-calendar"}, + { "sv4cpio", "application/x-sv4cpio"}, + { "sv4crc", "application/x-sv4crc"}, + { "svd", "application/vnd.svd"}, + { "svg", "image/svg+xml"}, + { "svgz", "image/svg+xml"}, + { "swa", "application/x-director"}, + { "swf", "application/x-shockwave-flash"}, + { "swi", "application/vnd.arastra.swi"}, + { "sxc", "application/vnd.sun.xml.calc"}, + { "sxd", "application/vnd.sun.xml.draw"}, + { "sxg", "application/vnd.sun.xml.writer.global"}, + { "sxi", "application/vnd.sun.xml.impress"}, + { "sxm", "application/vnd.sun.xml.math"}, + { "sxw", "application/vnd.sun.xml.writer"}, + { "tao", "application/vnd.tao.intent-module-archive"}, + { "t", "application/x-troff"}, + { "tar", "application/x-tar"}, + { "tcap", "application/vnd.3gpp2.tcap"}, + { "tcl", "application/x-tcl"}, + { "teacher", "application/vnd.smart.teacher"}, + { "tex", "application/x-tex"}, + { "texi", "application/x-texinfo"}, + { "texinfo", "application/x-texinfo"}, + { "text", "text/plain"}, + { "tfm", "application/x-tex-tfm"}, + { "tgz", "application/x-gzip"}, + { "tiff", "image/tiff"}, + { "tif", "image/tiff"}, + { "tmo", "application/vnd.tmobile-livetv"}, + { "torrent", "application/x-bittorrent"}, + { "tpl", "application/vnd.groove-tool-template"}, + { "tpt", "application/vnd.trid.tpt"}, + { "tra", "application/vnd.trueapp"}, + { "trm", "application/x-msterminal"}, + { "tr", "text/troff"}, + { "tsv", "text/tab-separated-values"}, + { "ttc", "application/x-font-ttf"}, + { "ttf", "application/x-font-ttf"}, + { "twd", "application/vnd.simtech-mindmapper"}, + { "twds", "application/vnd.simtech-mindmapper"}, + { "txd", "application/vnd.genomatix.tuxedo"}, + { "txf", "application/vnd.mobius.txf"}, + { "txt", "text/plain"}, + { "u32", "application/x-authorware-bin"}, + { "udeb", "application/x-debian-package"}, + { "ufd", "application/vnd.ufdl"}, + { "ufdl", "application/vnd.ufdl"}, + { "uls", "text/iuls"}, + { "umj", "application/vnd.umajin"}, + { "unityweb", "application/vnd.unity"}, + { "uoml", "application/vnd.uoml+xml"}, + { "uris", "text/uri-list"}, + { "uri", "text/uri-list"}, + { "urls", "text/uri-list"}, + { "ustar", "application/x-ustar"}, + { "utz", "application/vnd.uiq.theme"}, + { "uu", "text/x-uuencode"}, + { "vcd", "application/x-cdlink"}, + { "vcf", "text/x-vcard"}, + { "vcg", "application/vnd.groove-vcard"}, + { "vcs", "text/x-vcalendar"}, + { "vcx", "application/vnd.vcx"}, + { "vis", "application/vnd.visionary"}, + { "viv", "video/vnd.vivo"}, + { "vor", "application/vnd.stardivision.writer"}, + { "vox", "application/x-authorware-bin"}, + { "vrml", "x-world/x-vrml"}, + { "vsd", "application/vnd.visio"}, + { "vsf", "application/vnd.vsf"}, + { "vss", "application/vnd.visio"}, + { "vst", "application/vnd.visio"}, + { "vsw", "application/vnd.visio"}, + { "vtu", "model/vnd.vtu"}, + { "vxml", "application/voicexml+xml"}, + { "w3d", "application/x-director"}, + { "wad", "application/x-doom"}, + { "wav", "audio/x-wav"}, + { "wax", "audio/x-ms-wax"}, + { "wbmp", "image/vnd.wap.wbmp"}, + { "wbs", "application/vnd.criticaltools.wbs+xml"}, + { "wbxml", "application/vnd.wap.wbxml"}, + { "wcm", "application/vnd.ms-works"}, + { "wdb", "application/vnd.ms-works"}, + { "wiz", "application/msword"}, + { "wks", "application/vnd.ms-works"}, + { "wma", "audio/x-ms-wma"}, + { "wmd", "application/x-ms-wmd"}, + { "wmf", "application/x-msmetafile"}, + { "wmlc", "application/vnd.wap.wmlc"}, + { "wmlsc", "application/vnd.wap.wmlscriptc"}, + { "wmls", "text/vnd.wap.wmlscript"}, + { "wml", "text/vnd.wap.wml"}, + { "wm", "video/x-ms-wm"}, + { "wmv", "video/x-ms-wmv"}, + { "wmx", "video/x-ms-wmx"}, + { "wmz", "application/x-ms-wmz"}, + { "wpd", "application/vnd.wordperfect"}, + { "wpl", "application/vnd.ms-wpl"}, + { "wps", "application/vnd.ms-works"}, + { "wqd", "application/vnd.wqd"}, + { "wri", "application/x-mswrite"}, + { "wrl", "x-world/x-vrml"}, + { "wrz", "x-world/x-vrml"}, + { "wsdl", "application/wsdl+xml"}, + { "wspolicy", "application/wspolicy+xml"}, + { "wtb", "application/vnd.webturbo"}, + { "wvx", "video/x-ms-wvx"}, + { "x32", "application/x-authorware-bin"}, + { "x3d", "application/vnd.hzn-3d-crossword"}, + { "xaf", "x-world/x-vrml"}, + { "xap", "application/x-silverlight-app"}, + { "xar", "application/vnd.xara"}, + { "xbap", "application/x-ms-xbap"}, + { "xbd", "application/vnd.fujixerox.docuworks.binder"}, + { "xbm", "image/x-xbitmap"}, + { "xdm", "application/vnd.syncml.dm+xml"}, + { "xdp", "application/vnd.adobe.xdp+xml"}, + { "xdw", "application/vnd.fujixerox.docuworks"}, + { "xenc", "application/xenc+xml"}, + { "xer", "application/patch-ops-error+xml"}, + { "xfdf", "application/vnd.adobe.xfdf"}, + { "xfdl", "application/vnd.xfdl"}, + { "xht", "application/xhtml+xml"}, + { "xhtml", "application/xhtml+xml"}, + { "xhvml", "application/xv+xml"}, + { "xif", "image/vnd.xiff"}, + { "xla", "application/vnd.ms-excel"}, + { "xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, + { "xlb", "application/vnd.ms-excel"}, + { "xlc", "application/vnd.ms-excel"}, + { "xlm", "application/vnd.ms-excel"}, + { "xls", "application/vnd.ms-excel"}, + { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, + { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, + { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + { "xlt", "application/vnd.ms-excel"}, + { "xltm", "application/vnd.ms-excel.template.macroenabled.12"}, + { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + { "xlw", "application/vnd.ms-excel"}, + { "xml", "application/xml"}, + { "xo", "application/vnd.olpc-sugar"}, + { "xof", "x-world/x-vrml"}, + { "xop", "application/xop+xml"}, + { "xpdl", "application/xml"}, + { "xpi", "application/x-xpinstall"}, + { "xpm", "image/x-xpixmap"}, + { "xpr", "application/vnd.is-xpr"}, + { "xps", "application/vnd.ms-xpsdocument"}, + { "xpw", "application/vnd.intercon.formnet"}, + { "xpx", "application/vnd.intercon.formnet"}, + { "xsl", "application/xml"}, + { "xslt", "application/xslt+xml"}, + { "xsm", "application/vnd.syncml+xml"}, + { "xspf", "application/xspf+xml"}, + { "xul", "application/vnd.mozilla.xul+xml"}, + { "xvm", "application/xv+xml"}, + { "xvml", "application/xv+xml"}, + { "xwd", "image/x-xwindowdump"}, + { "xyz", "chemical/x-xyz"}, + { "z", "application/x-compress"}, + { "zaz", "application/vnd.zzazz.deck+xml"}, + { "zip", "application/zip"}, + { "zir", "application/vnd.zul"}, + { "zirz", "application/vnd.zul"}, + { "zmm", "application/vnd.handheld-entertainment+xml"} }; /** @@ -1204,31 +1207,34 @@ public class MimeUtility { } /** - * Convert some wrong MIME types encountered in the wild to canonical MIME - * types. + * Convert some wrong MIME types encountered in the wild to canonical MIME types. * - * @param mimeType The original MIME type - * @return If {@code mimeType} is known to be wrong the correct MIME type - * is returned. Otherwise the value of {@code mimeType} is returned - * unmodified. + * @param mimeType + * The original MIME type + * + * @return If {@code mimeType} is known to be wrong the correct MIME type is returned. + * Otherwise the lower case version of {@code mimeType} is returned. * * @see #MIME_TYPE_REPLACEMENT_MAP */ public static String canonicalizeMimeType(String mimeType) { + String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); for (String[] mimeTypeMapEntry : MIME_TYPE_REPLACEMENT_MAP) { - if (mimeTypeMapEntry[0].equals(mimeType)) { + if (mimeTypeMapEntry[0].equals(lowerCaseMimeType)) { return mimeTypeMapEntry[1]; } } - return mimeType; + return lowerCaseMimeType; } /** - * When viewing the attachment we want the MIME type to be as sensible as - * possible. So we fix it up if necessary. + * When viewing the attachment we want the MIME type to be as sensible as possible. So we fix + * it up if necessary. * - * @param mimeType The original MIME type of the attachment. - * @param name The (file)name of the attachment. + * @param mimeType + * The original MIME type of the attachment. + * @param name + * The (file)name of the attachment. * * @return The best MIME type we can come up with. */ @@ -1237,10 +1243,10 @@ public class MimeUtility { // If the MIME type is the generic "application/octet-stream" // we try to find a better one by looking at the file extension. return getMimeTypeByExtension(name); - } else { - // Some messages contain wrong MIME types. See if we know better. - return canonicalizeMimeType(mimeType); } + + // Some messages contain wrong MIME types. See if we know better. + return canonicalizeMimeType(mimeType); } private static Message getMessageFromPart(Part part) { diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index f3dbdf9cf..570cc2be5 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -16,6 +16,7 @@ import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; @@ -66,6 +67,7 @@ import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; @@ -75,6 +77,7 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; @@ -99,6 +102,8 @@ import org.apache.commons.io.IOUtils; *
*/ public class ImapStore extends Store { + public static final String STORE_TYPE = "IMAP"; + public static final int CONNECTION_SECURITY_NONE = 0; public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; @@ -131,6 +136,191 @@ public class ImapStore extends Store { private static final String[] EMPTY_STRING_ARRAY = new String[0]; + /** + * Decodes an ImapStore URI. + * + *

Possible forms:

+ *
+     * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
+     * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static ImapStoreSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String authenticationType = null; + String username = null; + String password = null; + String pathPrefix = null; + + URI imapUri; + try { + imapUri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid ImapStore URI", use); + } + + String scheme = imapUri.getScheme(); + if (scheme.equals("imap")) { + connectionSecurity = ConnectionSecurity.NONE; + port = 143; + } else if (scheme.equals("imap+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 143; + } else if (scheme.equals("imap+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 143; + } else if (scheme.equals("imap+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 993; + } else if (scheme.equals("imap+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 993; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = imapUri.getHost(); + + if (imapUri.getPort() != -1) { + port = imapUri.getPort(); + } + + if (imapUri.getUserInfo() != null) { + try { + String userinfo = imapUri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); + + if (userinfo.endsWith(":")) { + // Password is empty. This can only happen after an account was imported. + authenticationType = AuthType.valueOf(userInfoParts[0]).name(); + username = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } else if (userInfoParts.length == 2) { + authenticationType = AuthType.PLAIN.name(); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } else { + authenticationType = AuthType.valueOf(userInfoParts[0]).name(); + username = URLDecoder.decode(userInfoParts[1], "UTF-8"); + password = URLDecoder.decode(userInfoParts[2], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + String path = imapUri.getPath(); + if (path != null && path.length() > 0) { + pathPrefix = path.substring(1); + if (pathPrefix != null && pathPrefix.trim().length() == 0) { + pathPrefix = null; + } + } + + return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username, + password, pathPrefix); + } + + /** + * Creates an ImapStore URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return An ImapStore URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see ImapStore#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "imap+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "imap+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "imap+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "imap+tls+"; + break; + default: + case NONE: + scheme = "imap"; + break; + } + + AuthType authType; + try { + authType = AuthType.valueOf(server.authenticationType); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid authentication type: " + + server.authenticationType); + } + + String userInfo = authType.toString() + ":" + userEnc + ":" + passwordEnc; + try { + Map extra = server.getExtra(); + String prefix = (extra != null) ? extra.get(ImapStoreSettings.PATH_PREFIX_KEY) : null; + return new URI(scheme, userInfo, server.host, server.port, + prefix, + null, null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create ImapStore URI", e); + } + } + + /** + * This class is used to store the decoded contents of an ImapStore URI. + * + * @see ImapStore#decodeUri(String) + */ + private static class ImapStoreSettings extends ServerSettings { + private static final String PATH_PREFIX_KEY = "pathPrefix"; + + public final String pathPrefix; + + protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, + String authenticationType, String username, String password, String pathPrefix) { + super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, + password); + this.pathPrefix = pathPrefix; + } + + @Override + public Map getExtra() { + Map extra = new HashMap(); + putIfNotNull(extra, PATH_PREFIX_KEY, pathPrefix); + return extra; + } + + @Override + public ServerSettings newPassword(String newPassword) { + return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, + username, newPassword, pathPrefix); + } + } + + private String mHost; private int mPort; private String mUsername; @@ -228,74 +418,42 @@ public class ImapStore extends Store { */ private HashMap mFolderCache = new HashMap(); - /** - * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE - * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - * - * @param _uri - */ public ImapStore(Account account) throws MessagingException { super(account); - URI uri; + + ImapStoreSettings settings; try { - uri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid ImapStore URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = uri.getScheme(); - if (scheme.equals("imap")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 143; - } else if (scheme.equals("imap+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 143; - } else if (scheme.equals("imap+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 143; - } else if (scheme.equals("imap+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 993; - } else if (scheme.equals("imap+ssl")) { + break; + case SSL_TLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 993; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = uri.getHost(); + mAuthType = AuthType.valueOf(settings.authenticationType); + mUsername = settings.username; + mPassword = settings.password; - if (uri.getPort() != -1) { - mPort = uri.getPort(); - } - - if (uri.getUserInfo() != null) { - try { - String[] userInfoParts = uri.getUserInfo().split(":"); - if (userInfoParts.length == 2) { - mAuthType = AuthType.PLAIN; - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else { - mAuthType = AuthType.valueOf(userInfoParts[0]); - mUsername = URLDecoder.decode(userInfoParts[1], "UTF-8"); - mPassword = URLDecoder.decode(userInfoParts[2], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } - - if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { - mPathPrefix = uri.getPath().substring(1); - if (mPathPrefix != null && mPathPrefix.trim().length() == 0) { - mPathPrefix = null; - } - } + mPathPrefix = settings.pathPrefix; mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); } diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 0ce234fc0..56d9b5fd8 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -26,12 +26,19 @@ import java.util.HashSet; import java.util.List; public class Pop3Store extends Store { + public static final String STORE_TYPE = "POP3"; + public static final int CONNECTION_SECURITY_NONE = 0; public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + private enum AuthType { + PLAIN, + CRAM_MD5 + } + private static final String STLS_COMMAND = "STLS"; private static final String USER_COMMAND = "USER"; private static final String PASS_COMMAND = "PASS"; @@ -52,11 +59,150 @@ public class Pop3Store extends Store { private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; + /** + * Decodes a Pop3Store URI. + * + *

Possible forms:

+ *
+     * pop3://user:password@server:port CONNECTION_SECURITY_NONE
+     * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static ServerSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String username = null; + String password = null; + + URI pop3Uri; + try { + pop3Uri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid Pop3Store URI", use); + } + + String scheme = pop3Uri.getScheme(); + if (scheme.equals("pop3")) { + connectionSecurity = ConnectionSecurity.NONE; + port = 110; + } else if (scheme.equals("pop3+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 110; + } else if (scheme.equals("pop3+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 110; + } else if (scheme.equals("pop3+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 995; + } else if (scheme.equals("pop3+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 995; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = pop3Uri.getHost(); + + if (pop3Uri.getPort() != -1) { + port = pop3Uri.getPort(); + } + + String authType = AuthType.PLAIN.name(); + if (pop3Uri.getUserInfo() != null) { + try { + int userIndex = 0, passwordIndex = 1; + String userinfo = pop3Uri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); + if (userInfoParts.length > 2 || userinfo.endsWith(":") ) { + // If 'userinfo' ends with ":" the password is empty. This can only happen + // after an account was imported (so authType and username are present). + userIndex++; + passwordIndex++; + authType = userInfoParts[0]; + } + username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); + if (userInfoParts.length > passwordIndex) { + password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username, + password); + } + + /** + * Creates a Pop3Store URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A Pop3Store URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see Pop3Store#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "pop3+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "pop3+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "pop3+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "pop3+tls+"; + break; + default: + case NONE: + scheme = "pop3"; + break; + } + + try { + AuthType.valueOf(server.authenticationType); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid authentication type (" + + server.authenticationType + ")"); + } + + String userInfo = server.authenticationType + ":" + userEnc + ":" + passwordEnc; + try { + return new URI(scheme, userInfo, server.host, server.port, null, null, + null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create Pop3Store URI", e); + } + } + + private String mHost; private int mPort; private String mUsername; private String mPassword; - private boolean useCramMd5; + private AuthType mAuthType; private int mConnectionSecurity; private HashMap mFolders = new HashMap(); private Pop3Capabilities mCapabilities; @@ -68,68 +214,41 @@ public class Pop3Store extends Store { */ private boolean mTopNotSupported; - /** - * pop3://user:password@server:port CONNECTION_SECURITY_NONE - * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - */ + public Pop3Store(Account account) throws MessagingException { super(account); - URI uri; + ServerSettings settings; try { - uri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid Pop3Store URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = uri.getScheme(); - if (scheme.equals("pop3")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 110; - } else if (scheme.equals("pop3+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 110; - } else if (scheme.equals("pop3+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 110; - } else if (scheme.equals("pop3+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 995; - } else if (scheme.equals("pop3+ssl")) { + break; + case SSL_TLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 995; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = uri.getHost(); - - if (uri.getPort() != -1) { - mPort = uri.getPort(); - } - - useCramMd5 = false; - if (uri.getUserInfo() != null) { - try { - int userIndex = 0, passwordIndex = 1; - String[] userInfoParts = uri.getUserInfo().split(":"); - if (userInfoParts.length > 2) { - userIndex++; - passwordIndex++; - useCramMd5 = true; - } - mUsername = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); - if (userInfoParts.length > passwordIndex) { - mPassword = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } + mUsername = settings.username; + mPassword = settings.password; + mAuthType = AuthType.valueOf(settings.authenticationType); } @Override @@ -249,7 +368,7 @@ public class Pop3Store extends Store { } } - if (useCramMd5) { + if (mAuthType == AuthType.CRAM_MD5) { try { String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", ""); diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 8acc2ed02..17f3cc98b 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -56,6 +56,8 @@ import java.util.zip.GZIPInputStream; * */ public class WebDavStore extends Store { + public static final String STORE_TYPE = "WebDAV"; + // Security options private static final short CONNECTION_SECURITY_NONE = 0; private static final short CONNECTION_SECURITY_TLS_OPTIONAL = 1; @@ -84,12 +86,228 @@ public class WebDavStore extends Store { private static final String DAV_MAIL_OUTBOX_FOLDER = "outbox"; private static final String DAV_MAIL_SENT_FOLDER = "sentitems"; + + /** + * Decodes a WebDavStore URI. + * + *

Possible forms:

+ *
+     * webdav://user:password@server:port CONNECTION_SECURITY_NONE
+     * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static WebDavStoreSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String username = null; + String password = null; + String alias = null; + String path = null; + String authPath = null; + String mailboxPath = null; + + + URI webDavUri; + try { + webDavUri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid WebDavStore URI", use); + } + + String scheme = webDavUri.getScheme(); + if (scheme.equals("webdav")) { + connectionSecurity = ConnectionSecurity.NONE; + } else if (scheme.equals("webdav+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + } else if (scheme.equals("webdav+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + } else if (scheme.equals("webdav+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + } else if (scheme.equals("webdav+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = webDavUri.getHost(); + if (host.startsWith("http")) { + String[] hostParts = host.split("://", 2); + if (hostParts.length > 1) { + host = hostParts[1]; + } + } + + port = webDavUri.getPort(); + + String userInfo = webDavUri.getUserInfo(); + if (userInfo != null) { + try { + String[] userInfoParts = userInfo.split(":"); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); + String userParts[] = username.split("\\\\", 2); + + if (userParts.length > 1) { + alias = userParts[1]; + } else { + alias = username; + } + if (userInfoParts.length > 1) { + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + String[] pathParts = webDavUri.getPath().split("\\|"); + for (int i = 0, count = pathParts.length; i < count; i++) { + if (i == 0) { + if (pathParts[0] != null && + pathParts[0].length() > 1) { + path = pathParts[0]; + } + } else if (i == 1) { + if (pathParts[1] != null && + pathParts[1].length() > 1) { + authPath = pathParts[1]; + } + } else if (i == 2) { + if (pathParts[2] != null && + pathParts[2].length() > 1) { + mailboxPath = pathParts[2]; + } + } + } + + return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password, + alias, path, authPath, mailboxPath); + } + + /** + * Creates a WebDavStore URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A WebDavStore URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see WebDavStore#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "webdav+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "webdav+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "webdav+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "webdav+tls+"; + break; + default: + case NONE: + scheme = "webdav"; + break; + } + + String userInfo = userEnc + ":" + passwordEnc; + + String uriPath; + Map extra = server.getExtra(); + if (extra != null) { + String path = extra.get(WebDavStoreSettings.PATH_KEY); + path = (path != null) ? path : ""; + String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY); + authPath = (authPath != null) ? authPath : ""; + String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY); + mailboxPath = (mailboxPath != null) ? mailboxPath : ""; + uriPath = path + "|" + authPath + "|" + mailboxPath; + } else { + uriPath = "||"; + } + + try { + return new URI(scheme, userInfo, server.host, server.port, uriPath, + null, null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create WebDavStore URI", e); + } + } + + + /** + * This class is used to store the decoded contents of an WebDavStore URI. + * + * @see WebDavStore#decodeUri(String) + */ + private static class WebDavStoreSettings extends ServerSettings { + private static final String ALIAS_KEY = "alias"; + private static final String PATH_KEY = "path"; + private static final String AUTH_PATH_KEY = "authPath"; + private static final String MAILBOX_PATH_KEY = "mailboxPath"; + + public final String alias; + public final String path; + public final String authPath; + public final String mailboxPath; + + protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, + String authenticationType, String username, String password, String alias, + String path, String authPath, String mailboxPath) { + super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, + password); + this.alias = alias; + this.path = path; + this.authPath = authPath; + this.mailboxPath = mailboxPath; + } + + @Override + public Map getExtra() { + Map extra = new HashMap(); + putIfNotNull(extra, ALIAS_KEY, alias); + putIfNotNull(extra, PATH_KEY, path); + putIfNotNull(extra, AUTH_PATH_KEY, authPath); + putIfNotNull(extra, MAILBOX_PATH_KEY, mailboxPath); + return extra; + } + + @Override + public ServerSettings newPassword(String newPassword) { + return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType, + username, newPassword, alias, path, authPath, mailboxPath); + } + } + + private short mConnectionSecurity; private String mUsername; /* Stores the username for authentications */ private String mAlias; /* Stores the alias for the user's mailbox */ private String mPassword; /* Stores the password for authentications */ private String mUrl; /* Stores the base URL for the server */ private String mHost; /* Stores the host name for the server */ + private int mPort; private String mPath; /* Stores the path for the server */ private String mAuthPath; /* Stores the path off of the server to post data to for form based authentication */ private String mMailboxPath; /* Stores the user specified path to the mailbox */ @@ -106,85 +324,46 @@ public class WebDavStore extends Store { private Folder mSendFolder = null; private HashMap mFolderList = new HashMap(); - /** - * webdav://user:password@server:port CONNECTION_SECURITY_NONE - * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - */ + public WebDavStore(Account account) throws MessagingException { super(account); + WebDavStoreSettings settings; try { - mUri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid WebDavStore URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = mUri.getScheme(); - if (scheme.equals("webdav")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - } else if (scheme.equals("webdav+ssl")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - } else if (scheme.equals("webdav+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - } else if (scheme.equals("webdav+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - } else if (scheme.equals("webdav+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = mUri.getHost(); - if (mHost.startsWith("http")) { - String[] hostParts = mHost.split("://", 2); - if (hostParts.length > 1) { - mHost = hostParts[1]; - } - } + mUsername = settings.username; + mPassword = settings.password; + mAlias = settings.alias; - if (mUri.getUserInfo() != null) { - try { - String[] userInfoParts = mUri.getUserInfo().split(":"); - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); - String userParts[] = mUsername.split("\\\\", 2); + mPath = settings.path; + mAuthPath = settings.authPath; + mMailboxPath = settings.mailboxPath; - if (userParts.length > 1) { - mAlias = userParts[1]; - } else { - mAlias = mUsername; - } - if (userInfoParts.length > 1) { - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } - - String[] pathParts = mUri.getPath().split("\\|"); - - for (int i = 0, count = pathParts.length; i < count; i++) { - if (i == 0) { - if (pathParts[0] != null && - pathParts[0].length() > 1) { - mPath = pathParts[0]; - } - } else if (i == 1) { - if (pathParts[1] != null && - pathParts[1].length() > 1) { - mAuthPath = pathParts[1]; - } - } else if (i == 2) { - if (pathParts[2] != null && - pathParts[2].length() > 1) { - mMailboxPath = pathParts[2]; - } - } - } if (mPath == null || mPath.equals("")) { mPath = "/Exchange"; @@ -222,7 +401,7 @@ public class WebDavStore extends Store { } else { root = "http"; } - root += "://" + mHost + ":" + mUri.getPort(); + root += "://" + mHost + ":" + mPort; return root; } diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index e7546190a..81e9b258d 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -2,6 +2,7 @@ package com.fsck.k9.mail.transport; import android.util.Log; +import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.mail.*; import com.fsck.k9.mail.Message.RecipientType; @@ -29,14 +30,12 @@ import java.security.SecureRandom; import java.util.*; public class SmtpTransport extends Transport { + public static final String TRANSPORT_TYPE = "SMTP"; + public static final int CONNECTION_SECURITY_NONE = 0; - public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; - public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; - public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; - public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; public static final String AUTH_PLAIN = "PLAIN"; @@ -47,87 +46,187 @@ public class SmtpTransport extends Transport { public static final String AUTH_AUTOMATIC = "AUTOMATIC"; - String mHost; - - int mPort; - - String mUsername; - - String mPassword; - - String mAuthType; - - int mConnectionSecurity; - - boolean mSecure; - - Socket mSocket; - - PeekableInputStream mIn; - - OutputStream mOut; - private boolean m8bitEncodingAllowed; - - private int mLargestAcceptableMessage; /** + * Decodes a SmtpTransport URI. + * + *

Possible forms:

+ *
      * smtp://user:password@server:port CONNECTION_SECURITY_NONE
      * smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
      * smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
      * smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
      * smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
-     *
-     * @param _uri
+     * 
*/ - public SmtpTransport(String _uri) throws MessagingException { - URI uri; + public static ServerSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String authenticationType = AUTH_AUTOMATIC; + String username = null; + String password = null; + + URI smtpUri; try { - uri = new URI(_uri); + smtpUri = new URI(uri); } catch (URISyntaxException use) { - throw new MessagingException("Invalid SmtpTransport URI", use); + throw new IllegalArgumentException("Invalid SmtpTransport URI", use); } - String scheme = uri.getScheme(); + String scheme = smtpUri.getScheme(); if (scheme.equals("smtp")) { - mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 25; + connectionSecurity = ConnectionSecurity.NONE; + port = 25; } else if (scheme.equals("smtp+tls")) { - mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 25; + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 25; } else if (scheme.equals("smtp+tls+")) { - mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 25; + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 25; } else if (scheme.equals("smtp+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 465; + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 465; } else if (scheme.equals("smtp+ssl")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 465; + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 465; } else { - throw new MessagingException("Unsupported protocol"); + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); } - mHost = uri.getHost(); + host = smtpUri.getHost(); - if (uri.getPort() != -1) { - mPort = uri.getPort(); + if (smtpUri.getPort() != -1) { + port = smtpUri.getPort(); } - if (uri.getUserInfo() != null) { + if (smtpUri.getUserInfo() != null) { try { - String[] userInfoParts = uri.getUserInfo().split(":"); - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); + String[] userInfoParts = smtpUri.getUserInfo().split(":"); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); if (userInfoParts.length > 1) { - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); } if (userInfoParts.length > 2) { - mAuthType = userInfoParts[2]; + authenticationType = userInfoParts[2]; } } catch (UnsupportedEncodingException enc) { // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); } } + + return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity, + authenticationType, username, password); + } + + /** + * Creates a SmtpTransport URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A SmtpTransport URI that holds the same information as the {@code server} parameter. + * + * @see Account#getTransportUri() + * @see SmtpTransport#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = (server.username != null) ? + URLEncoder.encode(server.username, "UTF-8") : ""; + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "smtp+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "smtp+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "smtp+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "smtp+tls+"; + break; + default: + case NONE: + scheme = "smtp"; + break; + } + + String authType = server.authenticationType; + if (!(AUTH_AUTOMATIC.equals(authType) || + AUTH_LOGIN.equals(authType) || + AUTH_PLAIN.equals(authType) || + AUTH_CRAM_MD5.equals(authType))) { + throw new IllegalArgumentException("Invalid authentication type: " + authType); + } + + String userInfo = userEnc + ":" + passwordEnc + ":" + authType; + try { + return new URI(scheme, userInfo, server.host, server.port, null, null, + null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create SmtpTransport URI", e); + } + } + + + String mHost; + int mPort; + String mUsername; + String mPassword; + String mAuthType; + int mConnectionSecurity; + boolean mSecure; + Socket mSocket; + PeekableInputStream mIn; + OutputStream mOut; + private boolean m8bitEncodingAllowed; + private int mLargestAcceptableMessage; + + public SmtpTransport(String uri) throws MessagingException { + ServerSettings settings; + try { + settings = decodeUri(uri); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding transport URI", e); + } + + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: + mConnectionSecurity = CONNECTION_SECURITY_NONE; + break; + case STARTTLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; + break; + case STARTTLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; + break; + case SSL_TLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; + } + + mAuthType = settings.authenticationType; + mUsername = settings.username; + mPassword = settings.password; } @Override diff --git a/src/com/fsck/k9/mail/transport/WebDavTransport.java b/src/com/fsck/k9/mail/transport/WebDavTransport.java index 72159a5e6..6b5b61d60 100644 --- a/src/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/src/com/fsck/k9/mail/transport/WebDavTransport.java @@ -7,10 +7,38 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.store.WebDavStore; public class WebDavTransport extends Transport { + public static final String TRANSPORT_TYPE = WebDavStore.STORE_TYPE; + + /** + * Decodes a WebDavTransport URI. + * + *

+ * Note: Everything related to sending messages via WebDAV is handled by + * {@link WebDavStore}. So the transport URI is the same as the store URI. + *

+ */ + public static ServerSettings decodeUri(String uri) { + return WebDavStore.decodeUri(uri); + } + + /** + * Creates a WebDavTransport URI. + * + *

+ * Note: Everything related to sending messages via WebDAV is handled by + * {@link WebDavStore}. So the transport URI is the same as the store URI. + *

+ */ + public static String createUri(ServerSettings server) { + return WebDavStore.createUri(server); + } + + private WebDavStore store; public WebDavTransport(Account account) throws MessagingException { diff --git a/src/com/fsck/k9/preferences/AccountSettings.java b/src/com/fsck/k9/preferences/AccountSettings.java new file mode 100644 index 000000000..13c4d089a --- /dev/null +++ b/src/com/fsck/k9/preferences/AccountSettings.java @@ -0,0 +1,254 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.Account.FolderMode; +import com.fsck.k9.Account.ScrollButtons; +import com.fsck.k9.crypto.Apg; +import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.preferences.Settings.*; + +public class AccountSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("archiveFolderName", new StringSetting("Archive")); + s.put("autoExpandFolderName", new StringSetting("INBOX")); + s.put("automaticCheckIntervalMinutes", + new IntegerResourceSetting(-1, R.array.account_settings_check_frequency_values)); + s.put("chipColor", new ColorSetting(0xFF0000FF)); + s.put("cryptoApp", new StringSetting(Apg.NAME)); + s.put("cryptoAutoSignature", new BooleanSetting(false)); + s.put("defaultQuotedTextShown", new BooleanSetting(Account.DEFAULT_QUOTED_TEXT_SHOWN)); + s.put("deletePolicy", new DeletePolicySetting(Account.DELETE_POLICY_NEVER)); + s.put("displayCount", new IntegerResourceSetting(K9.DEFAULT_VISIBLE_LIMIT, + R.array.account_settings_display_count_values)); + s.put("draftsFolderName", new StringSetting("Drafts")); + s.put("enableMoveButtons", new BooleanSetting(false)); + s.put("expungePolicy", new StringResourceSetting(Account.EXPUNGE_IMMEDIATELY, + R.array.account_setup_expunge_policy_values)); + s.put("folderDisplayMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS)); + s.put("folderPushMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS)); + s.put("folderSyncMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS)); + s.put("folderTargetMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS)); + s.put("goToUnreadMessageSearch", new BooleanSetting(false)); + s.put("hideButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER)); + s.put("hideMoveButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER)); + s.put("idleRefreshMinutes", new IntegerResourceSetting(24, + R.array.idle_refresh_period_values)); + s.put("inboxFolderName", new StringSetting("INBOX")); + s.put("led", new BooleanSetting(true)); + s.put("ledColor", new ColorSetting(0xFF0000FF)); + s.put("localStorageProvider", new StorageProviderSetting()); + s.put("maxPushFolders", new IntegerRangeSetting(0, 100, 10)); + s.put("maximumAutoDownloadMessageSize", new IntegerResourceSetting(32768, + R.array.account_settings_autodownload_message_size_values)); + s.put("maximumPolledMessageAge", new IntegerResourceSetting(-1, + R.array.account_settings_message_age_values)); + s.put("messageFormat", + new EnumSetting(Account.MessageFormat.class, Account.DEFAULT_MESSAGE_FORMAT)); + s.put("messageReadReceipt", new BooleanSetting(Account.DEFAULT_MESSAGE_READ_RECEIPT)); + s.put("notificationUnreadCount", new BooleanSetting(true)); + s.put("notifyMailCheck", new BooleanSetting(false)); + s.put("notifyNewMail", new BooleanSetting(false)); + s.put("notifySelfNewMail", new BooleanSetting(true)); + s.put("pushPollOnConnect", new BooleanSetting(true)); + s.put("quotePrefix", new StringSetting(Account.DEFAULT_QUOTE_PREFIX)); + s.put("quoteStyle", + new EnumSetting(Account.QuoteStyle.class, Account.DEFAULT_QUOTE_STYLE)); + s.put("replyAfterQuote", new BooleanSetting(Account.DEFAULT_REPLY_AFTER_QUOTE)); + s.put("ring", new BooleanSetting(true)); + s.put("ringtone", new RingtoneSetting("content://settings/system/notification_sound")); + s.put("saveAllHeaders", new BooleanSetting(true)); + s.put("searchableFolders", + new EnumSetting(Account.Searchable.class, Account.Searchable.ALL)); + s.put("sentFolderName", new StringSetting("Sent")); + s.put("showPicturesEnum", + new EnumSetting(Account.ShowPictures.class, Account.ShowPictures.NEVER)); + s.put("signatureBeforeQuotedText", new BooleanSetting(false)); + s.put("spamFolderName", new StringSetting("Spam")); + s.put("subscribedFoldersOnly", new BooleanSetting(false)); + s.put("syncRemoteDeletions", new BooleanSetting(true)); + s.put("trashFolderName", new StringSetting("Trash")); + s.put("useCompression.MOBILE", new BooleanSetting(true)); + s.put("useCompression.OTHER", new BooleanSetting(true)); + s.put("useCompression.WIFI", new BooleanSetting(true)); + s.put("vibrate", new BooleanSetting(false)); + s.put("vibratePattern", new IntegerResourceSetting(0, + R.array.account_settings_vibrate_pattern_values)); + s.put("vibrateTimes", new IntegerResourceSetting(5, + R.array.account_settings_vibrate_times_label)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getAccountSettings(SharedPreferences storage, String uuid) { + Map result = new HashMap(); + String prefix = uuid + "."; + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + /** + * An integer resource setting. + * + *

+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array containing + * integer strings. + *

+ */ + public static class IntegerResourceSetting extends PseudoEnumSetting { + private final Map mMapping; + + public IntegerResourceSetting(int defaultValue, int resId) { + super(defaultValue); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(resId); + for (String value : values) { + int intValue = Integer.parseInt(value); + mapping.put(intValue, value); + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A string resource setting. + * + *

+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array. + *

+ */ + public static class StringResourceSetting extends PseudoEnumSetting { + private final Map mMapping; + + public StringResourceSetting(String defaultValue, int resId) { + super(defaultValue); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(resId); + for (String value : values) { + mapping.put(value, value); + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (!mMapping.containsKey(value)) { + throw new InvalidSettingValueException(); + } + return value; + } + } + + /** + * The notification ringtone setting. + */ + public static class RingtoneSetting extends SettingsDescription { + public RingtoneSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) { + //TODO: add validation + return value; + } + } + + /** + * The storage provider setting. + */ + public static class StorageProviderSetting extends SettingsDescription { + public StorageProviderSetting() { + super(null); + } + + @Override + public Object getDefaultValue() { + return StorageManager.getInstance(K9.app).getDefaultProviderId(); + } + + @Override + public Object fromString(String value) { + StorageManager storageManager = StorageManager.getInstance(K9.app); + Map providers = storageManager.getAvailableProviders(); + if (providers.containsKey(value)) { + return value; + } + throw new RuntimeException("Validation failed"); + } + } + + /** + * The delete policy setting. + */ + public static class DeletePolicySetting extends PseudoEnumSetting { + private Map mMapping; + + public DeletePolicySetting(int defaultValue) { + super(defaultValue); + Map mapping = new HashMap(); + mapping.put(Account.DELETE_POLICY_NEVER, "NEVER"); + mapping.put(Account.DELETE_POLICY_ON_DELETE, "DELETE"); + mapping.put(Account.DELETE_POLICY_MARK_AS_READ, "MARK_AS_READ"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer deletePolicy = Integer.parseInt(value); + if (mMapping.containsKey(deletePolicy)) { + return deletePolicy; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/FolderSettings.java b/src/com/fsck/k9/preferences/FolderSettings.java new file mode 100644 index 000000000..1a2af5502 --- /dev/null +++ b/src/com/fsck/k9/preferences/FolderSettings.java @@ -0,0 +1,44 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; + +import com.fsck.k9.mail.Folder.FolderClass; +import com.fsck.k9.preferences.Settings.*; + +public class FolderSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("displayMode", new EnumSetting(FolderClass.class, FolderClass.NO_CLASS)); + s.put("syncMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED)); + s.put("pushMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED)); + s.put("inTopGroup", new BooleanSetting(false)); + s.put("integrate", new BooleanSetting(false)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getFolderSettings(SharedPreferences storage, String uuid, + String folderName) { + Map result = new HashMap(); + String prefix = uuid + "." + folderName + "."; + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } +} diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java new file mode 100644 index 000000000..746d96dbd --- /dev/null +++ b/src/com/fsck/k9/preferences/GlobalSettings.java @@ -0,0 +1,256 @@ +package com.fsck.k9.preferences; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; +import android.os.Environment; + +import com.fsck.k9.FontSizes; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.helper.DateFormatter; +import com.fsck.k9.preferences.Settings.*; + +public class GlobalSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("animations", new BooleanSetting(false)); + s.put("attachmentdefaultpath", + new DirectorySetting(Environment.getExternalStorageDirectory().toString())); + s.put("backgroundOperations", + new EnumSetting(K9.BACKGROUND_OPS.class, K9.BACKGROUND_OPS.WHEN_CHECKED)); + s.put("changeRegisteredNameColor", new BooleanSetting(false)); + s.put("compactLayouts", new BooleanSetting(false)); + s.put("confirmDelete", new BooleanSetting(false)); + s.put("confirmMarkAllAsRead", new BooleanSetting(false)); + s.put("confirmSpam", new BooleanSetting(false)); + s.put("countSearchMessages", new BooleanSetting(false)); + s.put("dateFormat", new DateFormatSetting(DateFormatter.DEFAULT_FORMAT)); + s.put("enableDebugLogging", new BooleanSetting(false)); + s.put("enableSensitiveLogging", new BooleanSetting(false)); + s.put("fontSizeAccountDescription", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeAccountName", new FontSizeSetting(FontSizes.MEDIUM)); + s.put("fontSizeFolderName", new FontSizeSetting(FontSizes.LARGE)); + s.put("fontSizeFolderStatus", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListDate", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListPreview", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListSender", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListSubject", new FontSizeSetting(FontSizes.FONT_16DIP)); + s.put("fontSizeMessageViewAdditionalHeaders", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewCC", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewContent", new WebFontSizeSetting(3)); + s.put("fontSizeMessageViewDate", new FontSizeSetting(FontSizes.FONT_10DIP)); + s.put("fontSizeMessageViewSender", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageViewSubject", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewTime", new FontSizeSetting(FontSizes.FONT_10DIP)); + s.put("fontSizeMessageViewTo", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("gesturesEnabled", new BooleanSetting(true)); + s.put("hideSpecialAccounts", new BooleanSetting(false)); + s.put("keyguardPrivacy", new BooleanSetting(false)); + s.put("language", new LanguageSetting()); + s.put("manageBack", new BooleanSetting(false)); + s.put("measureAccounts", new BooleanSetting(true)); + s.put("messageListCheckboxes", new BooleanSetting(false)); + s.put("messageListPreviewLines", new IntegerRangeSetting(1, 100, 2)); + s.put("messageListStars", new BooleanSetting(true)); + s.put("messageListTouchable", new BooleanSetting(false)); + s.put("messageViewFixedWidthFont", new BooleanSetting(false)); + s.put("messageViewReturnToList", new BooleanSetting(false)); + s.put("messageViewShowNext", new BooleanSetting(false)); + s.put("mobileOptimizedLayout", new BooleanSetting(false)); + s.put("quietTimeEnabled", new BooleanSetting(false)); + s.put("quietTimeEnds", new TimeSetting("7:00")); + s.put("quietTimeStarts", new TimeSetting("21:00")); + s.put("registeredNameColor", new ColorSetting(0xFF00008F)); + s.put("showContactName", new BooleanSetting(false)); + s.put("showCorrespondentNames", new BooleanSetting(true)); + s.put("startIntegratedInbox", new BooleanSetting(false)); + s.put("theme", new ThemeSetting(android.R.style.Theme_Light)); + s.put("useGalleryBugWorkaround", new GalleryBugWorkaroundSetting()); + s.put("useVolumeKeysForListNavigation", new BooleanSetting(false)); + s.put("useVolumeKeysForNavigation", new BooleanSetting(false)); + s.put("zoomControlsEnabled", new BooleanSetting(false)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings) { + return Settings.validate(SETTINGS, importedSettings, false); + } + + public static Map getGlobalSettings(SharedPreferences storage) { + Map result = new HashMap(); + for (String key : SETTINGS.keySet()) { + String value = storage.getString(key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + /** + * The gallery bug work-around setting. + * + *

+ * The default value varies depending on whether you have a version of Gallery 3D installed + * that contains the bug we work around. + *

+ * + * @see K9#isGalleryBuggy() + */ + public static class GalleryBugWorkaroundSetting extends BooleanSetting { + public GalleryBugWorkaroundSetting() { + super(false); + } + + @Override + public Object getDefaultValue() { + return K9.isGalleryBuggy(); + } + } + + /** + * The language setting. + * + *

+ * Valid values are read from {@code settings_language_values} in + * {@code res/values/arrays.xml}. + *

+ */ + public static class LanguageSetting extends PseudoEnumSetting { + private final Map mMapping; + + public LanguageSetting() { + super(""); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(R.array.settings_language_values); + for (String value : values) { + if (value.length() == 0) { + mapping.put("", "default"); + } else { + mapping.put(value, value); + } + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (mMapping.containsKey(value)) { + return value; + } + + throw new InvalidSettingValueException(); + } + } + + /** + * The theme setting. + */ + public static class ThemeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public ThemeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(android.R.style.Theme_Light, "light"); + mapping.put(android.R.style.Theme, "dark"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer theme = Integer.parseInt(value); + if (mMapping.containsKey(theme)) { + return theme; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * A date format setting. + */ + public static class DateFormatSetting extends SettingsDescription { + public DateFormatSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + // The placeholders "SHORT" and "MEDIUM" are fine. + if (DateFormatter.SHORT_FORMAT.equals(value) || + DateFormatter.MEDIUM_FORMAT.equals(value)) { + return value; + } + + // If the SimpleDateFormat constructor doesn't throw an exception, we're good. + new SimpleDateFormat(value); + return value; + } catch (Exception e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A time setting. + */ + public static class TimeSetting extends SettingsDescription { + public TimeSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (!value.matches(TimePickerPreference.VALIDATION_EXPRESSION)) { + throw new InvalidSettingValueException(); + } + return value; + } + } + + /** + * A directory on the file system. + */ + public static class DirectorySetting extends SettingsDescription { + public DirectorySetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + if (new File(value).isDirectory()) { + return value; + } + } catch (Exception e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/IdentitySettings.java b/src/com/fsck/k9/preferences/IdentitySettings.java new file mode 100644 index 000000000..f796446e1 --- /dev/null +++ b/src/com/fsck/k9/preferences/IdentitySettings.java @@ -0,0 +1,99 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; + +import com.fsck.k9.EmailAddressValidator; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.preferences.Settings.*; + +public class IdentitySettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("signature", new SignatureSetting()); + s.put("signatureUse", new BooleanSetting(true)); + s.put("replyTo", new OptionalEmailAddressSetting()); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getIdentitySettings(SharedPreferences storage, String uuid, + int identityIndex) { + Map result = new HashMap(); + String prefix = uuid + "."; + String suffix = "." + Integer.toString(identityIndex); + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key + suffix, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + + public static boolean isEmailAddressValid(String email) { + return new EmailAddressValidator().isValidAddressOnly(email); + } + + /** + * The message signature setting. + */ + public static class SignatureSetting extends SettingsDescription { + public SignatureSetting() { + super(null); + } + + @Override + public Object getDefaultValue() { + return K9.app.getResources().getString(R.string.default_signature); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + return value; + } + } + + /** + * An optional email address setting. + */ + public static class OptionalEmailAddressSetting extends SettingsDescription { + private EmailAddressValidator mValidator; + + public OptionalEmailAddressSetting() { + super(null); + mValidator = new EmailAddressValidator(); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (value != null && !mValidator.isValidAddressOnly(value)) { + throw new InvalidSettingValueException(); + } + return value; + } + + @Override + public String toPrettyString(Object value) { + return (value == null) ? "" : value.toString(); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + return ("".equals(value)) ? null : fromString(value); + } + } +} diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java new file mode 100644 index 000000000..8b2b00ace --- /dev/null +++ b/src/com/fsck/k9/preferences/Settings.java @@ -0,0 +1,409 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import android.util.Log; + +import com.fsck.k9.FontSizes; +import com.fsck.k9.K9; + +/* + * TODO: + * - add support for different settings versions (validate old version and upgrade to new format) + * - use the default values defined in GlobalSettings and AccountSettings when creating new + * accounts + * - think of a better way to validate enums than to use the resource arrays (i.e. get rid of + * ResourceArrayValidator); maybe even use the settings description for the settings UI + * - add unit test that validates the default values are actually valid according to the validator + */ + +public class Settings { + /** + * Version number of global and account settings. + * + *

+ * This value is used as "version" attribute in the export file. It needs to be incremented + * when a global or account setting is added or removed, or when the format of a setting + * is changed (e.g. add a value to an enum). + *

+ * + * @see SettingsExporter + */ + public static final int VERSION = 1; + + public static Map validate(Map settings, + Map importedSettings, boolean useDefaultValues) { + + Map validatedSettings = new HashMap(); + for (Map.Entry setting : settings.entrySet()) { + String key = setting.getKey(); + SettingsDescription desc = setting.getValue(); + + boolean useDefaultValue; + if (!importedSettings.containsKey(key)) { + Log.v(K9.LOG_TAG, "Key \"" + key + "\" wasn't found in the imported file." + + ((useDefaultValues) ? " Using default value." : "")); + useDefaultValue = useDefaultValues; + } else { + String prettyValue = importedSettings.get(key); + try { + Object internalValue = desc.fromPrettyString(prettyValue); + String importedValue = desc.toString(internalValue); + validatedSettings.put(key, importedValue); + useDefaultValue = false; + } catch (InvalidSettingValueException e) { + Log.v(K9.LOG_TAG, "Key \"" + key + "\" has invalid value \"" + prettyValue + + "\" in imported file. " + + ((useDefaultValues) ? "Using default value." : "Skipping.")); + useDefaultValue = useDefaultValues; + } + } + + if (useDefaultValue) { + Object defaultValue = desc.getDefaultValue(); + String value = (defaultValue != null) ? desc.toString(defaultValue) : null; + validatedSettings.put(key, value); + } + } + + return validatedSettings; + } + + + /** + * Indicates an invalid setting value. + * + * @see SettingsDescription#fromString(String) + * @see SettingsDescription#fromPrettyString(String) + */ + public static class InvalidSettingValueException extends Exception { + private static final long serialVersionUID = 1L; + } + + /** + * Describes a setting. + * + *

+ * Instances of this class are used to convert the string representations of setting values to + * an internal representation (e.g. an integer) and back. + *

+ * Currently we use two different string representations: + *

+ *
    + *
  1. + * The one that is used by the internal preference {@link Storage}. It is usually obtained by + * calling {@code toString()} on the internal representation of the setting value (see e.g. + * {@link K9#save(android.content.SharedPreferences.Editor)}). + *
  2. + *
  3. + * The "pretty" version that is used by the import/export settings file (e.g. colors are + * exported in #rrggbb format instead of a integer string like "-8734021"). + *
  4. + *
+ *

+ * Note: + * For the future we should aim to get rid of the "internal" string representation. The + * "pretty" version makes reading a database dump easier and the performance impact should be + * negligible. + *

+ */ + public static abstract class SettingsDescription { + /** + * The setting's default value (internal representation). + */ + protected Object mDefaultValue; + + public SettingsDescription(Object defaultValue) { + mDefaultValue = defaultValue; + } + + /** + * Get the default value. + * + * @return The internal representation of the default value. + */ + public Object getDefaultValue() { + return mDefaultValue; + } + + /** + * Convert a setting's value to the string representation. + * + * @param value + * The internal representation of a setting's value. + * + * @return The string representation of {@code value}. + */ + public String toString(Object value) { + return value.toString(); + } + + /** + * Parse the string representation of a setting's value . + * + * @param value + * The string representation of a setting's value. + * + * @return The internal representation of the setting's value. + * + * @throws InvalidSettingValueException + * If {@code value} contains an invalid value. + */ + public abstract Object fromString(String value) throws InvalidSettingValueException; + + /** + * Convert a setting value to the "pretty" string representation. + * + * @param value + * The setting's value. + * + * @return A pretty-printed version of the setting's value. + */ + public String toPrettyString(Object value) { + return toString(value); + } + + /** + * Convert the pretty-printed version of a setting's value to the internal representation. + * + * @param value + * The pretty-printed version of the setting's value. See + * {@link #toPrettyString(Object)}. + * + * @return The internal representation of the setting's value. + * + * @throws InvalidSettingValueException + * If {@code value} contains an invalid value. + */ + public Object fromPrettyString(String value) throws InvalidSettingValueException { + return fromString(value); + } + } + + /** + * A string setting. + */ + public static class StringSetting extends SettingsDescription { + public StringSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) { + return value; + } + } + + /** + * A boolean setting. + */ + public static class BooleanSetting extends SettingsDescription { + public BooleanSetting(boolean defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (Boolean.TRUE.toString().equals(value)) { + return true; + } else if (Boolean.FALSE.toString().equals(value)) { + return false; + } + throw new InvalidSettingValueException(); + } + } + + /** + * A color setting. + */ + public static class ColorSetting extends SettingsDescription { + public ColorSetting(int defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new InvalidSettingValueException(); + } + } + + @Override + public String toPrettyString(Object value) { + int color = ((Integer) value) & 0x00FFFFFF; + return String.format("#%06x", color); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + try { + if (value.length() == 7) { + return Integer.parseInt(value.substring(1), 16) | 0xFF000000; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * An {@code Enum} setting. + * + *

+ * {@link Enum#toString()} is used to obtain the "pretty" string representation. + *

+ */ + public static class EnumSetting extends SettingsDescription { + private Class> mEnumClass; + + public EnumSetting(Class> enumClass, Object defaultValue) { + super(defaultValue); + mEnumClass = enumClass; + } + + @SuppressWarnings("unchecked") + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Enum.valueOf((Class)mEnumClass, value); + } catch (Exception e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A setting that has multiple valid values but doesn't use an {@link Enum} internally. + * + * @param + * The type of the internal representation (e.g. {@code Integer}). + */ + public abstract static class PseudoEnumSetting extends SettingsDescription { + public PseudoEnumSetting(Object defaultValue) { + super(defaultValue); + } + + protected abstract Map getMapping(); + + @Override + public String toPrettyString(Object value) { + return getMapping().get(value); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + for (Entry entry : getMapping().entrySet()) { + if (entry.getValue().equals(value)) { + return entry.getKey(); + } + } + + throw new InvalidSettingValueException(); + } + } + + /** + * A font size setting. + */ + public static class FontSizeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public FontSizeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(FontSizes.FONT_10DIP, "tiniest"); + mapping.put(FontSizes.FONT_12DIP, "tiny"); + mapping.put(FontSizes.SMALL, "smaller"); + mapping.put(FontSizes.FONT_16DIP, "small"); + mapping.put(FontSizes.MEDIUM, "medium"); + mapping.put(FontSizes.FONT_20DIP, "large"); + mapping.put(FontSizes.LARGE, "larger"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer fontSize = Integer.parseInt(value); + if (mMapping.containsKey(fontSize)) { + return fontSize; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * A {@link android.webkit.WebView} font size setting. + */ + public static class WebFontSizeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public WebFontSizeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(1, "smallest"); + mapping.put(2, "smaller"); + mapping.put(3, "normal"); + mapping.put(4, "larger"); + mapping.put(5, "largest"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer fontSize = Integer.parseInt(value); + if (mMapping.containsKey(fontSize)) { + return fontSize; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * An integer settings whose values a limited to a certain range. + */ + public static class IntegerRangeSetting extends SettingsDescription { + private int mStart; + private int mEnd; + + public IntegerRangeSetting(int start, int end, int defaultValue) { + super(defaultValue); + mStart = start; + mEnd = end; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + int intValue = Integer.parseInt(value); + if (mStart <= intValue && intValue <= mEnd) { + return intValue; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java new file mode 100644 index 000000000..cabc8cdf2 --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsExporter.java @@ -0,0 +1,483 @@ +package com.fsck.k9.preferences; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import org.xmlpull.v1.XmlSerializer; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.util.Log; +import android.util.Xml; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.helper.Utility; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.preferences.Settings.InvalidSettingValueException; +import com.fsck.k9.preferences.Settings.SettingsDescription; + + +public class SettingsExporter { + private static final String EXPORT_FILENAME = "settings.k9s"; + + /** + * File format version number. + * + *

+ * Increment this if you need to change the structure of the settings file. When you do this + * remember that we also have to be able to handle old file formats. So have fun adding support + * for that to {@link SettingsImporter} :) + *

+ */ + public static final int FILE_FORMAT_VERSION = 1; + + public static final String ROOT_ELEMENT = "k9settings"; + public static final String VERSION_ATTRIBUTE = "version"; + public static final String FILE_FORMAT_ATTRIBUTE = "format"; + public static final String GLOBAL_ELEMENT = "global"; + public static final String SETTINGS_ELEMENT = "settings"; + public static final String ACCOUNTS_ELEMENT = "accounts"; + public static final String ACCOUNT_ELEMENT = "account"; + public static final String UUID_ATTRIBUTE = "uuid"; + public static final String INCOMING_SERVER_ELEMENT = "incoming-server"; + public static final String OUTGOING_SERVER_ELEMENT = "outgoing-server"; + public static final String TYPE_ATTRIBUTE = "type"; + public static final String HOST_ELEMENT = "host"; + public static final String PORT_ELEMENT = "port"; + public static final String CONNECTION_SECURITY_ELEMENT = "connection-security"; + public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type"; + public static final String USERNAME_ELEMENT = "username"; + public static final String PASSWORD_ELEMENT = "password"; + public static final String EXTRA_ELEMENT = "extra"; + public static final String IDENTITIES_ELEMENT = "identities"; + public static final String IDENTITY_ELEMENT = "identity"; + public static final String FOLDERS_ELEMENT = "folders"; + public static final String FOLDER_ELEMENT = "folder"; + public static final String NAME_ATTRIBUTE = "name"; + public static final String VALUE_ELEMENT = "value"; + public static final String KEY_ATTRIBUTE = "key"; + public static final String NAME_ELEMENT = "name"; + public static final String EMAIL_ELEMENT = "email"; + public static final String DESCRIPTION_ELEMENT = "description"; + + + public static String exportToFile(Context context, boolean includeGlobals, + Set accountUuids) + throws SettingsImportExportException { + + OutputStream os = null; + String filename = null; + try + { + File dir = new File(Environment.getExternalStorageDirectory() + File.separator + + context.getPackageName()); + dir.mkdirs(); + File file = Utility.createUniqueFile(dir, EXPORT_FILENAME); + filename = file.getAbsolutePath(); + os = new FileOutputStream(filename); + + exportPreferences(context, os, includeGlobals, accountUuids); + + // If all went well, we return the name of the file just written. + return filename; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException ioe) { + Log.w(K9.LOG_TAG, "Couldn't close exported settings file: " + filename); + } + } + } + } + + public static void exportPreferences(Context context, OutputStream os, boolean includeGlobals, + Set accountUuids) throws SettingsImportExportException { + + try { + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(os, "UTF-8"); + + serializer.startDocument(null, Boolean.valueOf(true)); + + // Output with indentation + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + serializer.startTag(null, ROOT_ELEMENT); + serializer.attribute(null, VERSION_ATTRIBUTE, Integer.toString(Settings.VERSION)); + serializer.attribute(null, FILE_FORMAT_ATTRIBUTE, + Integer.toString(FILE_FORMAT_VERSION)); + + Log.i(K9.LOG_TAG, "Exporting preferences"); + + Preferences preferences = Preferences.getPreferences(context); + SharedPreferences storage = preferences.getPreferences(); + + Set exportAccounts; + if (accountUuids == null) { + Account[] accounts = preferences.getAccounts(); + exportAccounts = new HashSet(); + for (Account account : accounts) { + exportAccounts.add(account.getUuid()); + } + } else { + exportAccounts = accountUuids; + } + + Map prefs = new TreeMap(storage.getAll()); + + if (includeGlobals) { + serializer.startTag(null, GLOBAL_ELEMENT); + writeSettings(serializer, prefs); + serializer.endTag(null, GLOBAL_ELEMENT); + } + + serializer.startTag(null, ACCOUNTS_ELEMENT); + for (String accountUuid : exportAccounts) { + Account account = preferences.getAccount(accountUuid); + writeAccount(serializer, account, prefs); + } + serializer.endTag(null, ACCOUNTS_ELEMENT); + + serializer.endTag(null, ROOT_ELEMENT); + serializer.endDocument(); + serializer.flush(); + + } catch (Exception e) { + throw new SettingsImportExportException(e.getLocalizedMessage(), e); + } + } + + private static void writeSettings(XmlSerializer serializer, + Map prefs) throws IOException { + + for (String key : GlobalSettings.SETTINGS.keySet()) { + String valueString = (String) prefs.get(key); + if (valueString != null) { + try { + SettingsDescription setting = GlobalSettings.SETTINGS.get(key); + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, key, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Global setting \"" + key + "\" has invalid value \"" + + valueString + "\" in preference storage. This shouldn't happen!"); + } + } else { + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Couldn't find key \"" + key + "\" in preference storage." + + "Using default value."); + } + + SettingsDescription setting = GlobalSettings.SETTINGS.get(key); + Object value = setting.getDefaultValue(); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, key, outputValue); + } + } + } + + private static void writeAccount(XmlSerializer serializer, Account account, + Map prefs) throws IOException { + + Set identities = new HashSet(); + Set folders = new HashSet(); + String accountUuid = account.getUuid(); + + serializer.startTag(null, ACCOUNT_ELEMENT); + serializer.attribute(null, UUID_ATTRIBUTE, accountUuid); + + String name = (String) prefs.get(accountUuid + "." + Account.ACCOUNT_DESCRIPTION_KEY); + if (name != null) { + serializer.startTag(null, NAME_ELEMENT); + serializer.text(name); + serializer.endTag(null, NAME_ELEMENT); + } + + + // Write incoming server settings + ServerSettings incoming = Store.decodeStoreUri(account.getStoreUri()); + serializer.startTag(null, INCOMING_SERVER_ELEMENT); + serializer.attribute(null, TYPE_ATTRIBUTE, incoming.type); + + writeElement(serializer, HOST_ELEMENT, incoming.host); + if (incoming.port != -1) { + writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port)); + } + writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name()); + writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType); + writeElement(serializer, USERNAME_ELEMENT, incoming.username); + // XXX For now we don't export the password + //writeElement(serializer, PASSWORD_ELEMENT, incoming.password); + + Map extras = incoming.getExtra(); + if (extras != null && extras.size() > 0) { + serializer.startTag(null, EXTRA_ELEMENT); + for (Entry extra : extras.entrySet()) { + writeKeyValue(serializer, extra.getKey(), extra.getValue()); + } + serializer.endTag(null, EXTRA_ELEMENT); + } + + serializer.endTag(null, INCOMING_SERVER_ELEMENT); + + + // Write outgoing server settings + ServerSettings outgoing = Transport.decodeTransportUri(account.getTransportUri()); + serializer.startTag(null, OUTGOING_SERVER_ELEMENT); + serializer.attribute(null, TYPE_ATTRIBUTE, outgoing.type); + + writeElement(serializer, HOST_ELEMENT, outgoing.host); + if (outgoing.port != -1) { + writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port)); + } + writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name()); + writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType); + writeElement(serializer, USERNAME_ELEMENT, outgoing.username); + // XXX For now we don't export the password + //writeElement(serializer, PASSWORD_ELEMENT, outgoing.password); + + extras = outgoing.getExtra(); + if (extras != null && extras.size() > 0) { + serializer.startTag(null, EXTRA_ELEMENT); + for (Entry extra : extras.entrySet()) { + writeKeyValue(serializer, extra.getKey(), extra.getValue()); + } + serializer.endTag(null, EXTRA_ELEMENT); + } + + serializer.endTag(null, OUTGOING_SERVER_ELEMENT); + + + // Write account settings + serializer.startTag(null, SETTINGS_ELEMENT); + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 2) { + // Skip global settings + continue; + } + + String keyUuid = comps[0]; + String secondPart = comps[1]; + + if (!keyUuid.equals(accountUuid)) { + // Setting doesn't belong to the account we're currently writing. + continue; + } + + String keyPart; + if (comps.length >= 3) { + String thirdPart = comps[2]; + + if (Account.IDENTITY_DESCRIPTION_KEY.equals(secondPart)) { + // This is an identity key. Save identity index for later... + try { + identities.add(Integer.parseInt(thirdPart)); + } catch (NumberFormatException e) { /* ignore */ } + // ... but don't write it now. + continue; + } + + if (FolderSettings.SETTINGS.containsKey(thirdPart)) { + // This is a folder key. Save folder name for later... + folders.add(secondPart); + // ... but don't write it now. + continue; + } + + // Strip account UUID from key + keyPart = key.substring(comps[0].length() + 1); + } else { + keyPart = secondPart; + } + + SettingsDescription setting = AccountSettings.SETTINGS.get(keyPart); + if (setting != null) { + // Only export account settings that can be found in AccountSettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String pretty = setting.toPrettyString(value); + writeKeyValue(serializer, keyPart, pretty); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Account setting \"" + keyPart + "\" (" + + account.getDescription() + ") has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + serializer.endTag(null, SETTINGS_ELEMENT); + + if (identities.size() > 0) { + serializer.startTag(null, IDENTITIES_ELEMENT); + + // Sort identity indices (that's why we store them as Integers) + List sortedIdentities = new ArrayList(identities); + Collections.sort(sortedIdentities); + + for (Integer identityIndex : sortedIdentities) { + writeIdentity(serializer, accountUuid, identityIndex.toString(), prefs); + } + serializer.endTag(null, IDENTITIES_ELEMENT); + } + + if (folders.size() > 0) { + serializer.startTag(null, FOLDERS_ELEMENT); + for (String folder : folders) { + writeFolder(serializer, accountUuid, folder, prefs); + } + serializer.endTag(null, FOLDERS_ELEMENT); + } + + serializer.endTag(null, ACCOUNT_ELEMENT); + } + + private static void writeIdentity(XmlSerializer serializer, String accountUuid, + String identity, Map prefs) throws IOException { + + serializer.startTag(null, IDENTITY_ELEMENT); + + String prefix = accountUuid + "."; + String suffix = "." + identity; + + // Write name belonging to the identity + String name = (String) prefs.get(prefix + Account.IDENTITY_NAME_KEY + suffix); + serializer.startTag(null, NAME_ELEMENT); + serializer.text(name); + serializer.endTag(null, NAME_ELEMENT); + + // Write email address belonging to the identity + String email = (String) prefs.get(prefix + Account.IDENTITY_EMAIL_KEY + suffix); + serializer.startTag(null, EMAIL_ELEMENT); + serializer.text(email); + serializer.endTag(null, EMAIL_ELEMENT); + + // Write identity description + String description = (String) prefs.get(prefix + Account.IDENTITY_DESCRIPTION_KEY + suffix); + if (description != null) { + serializer.startTag(null, DESCRIPTION_ELEMENT); + serializer.text(description); + serializer.endTag(null, DESCRIPTION_ELEMENT); + } + + // Write identity settings + serializer.startTag(null, SETTINGS_ELEMENT); + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 3) { + // Skip non-identity config entries + continue; + } + + String keyUuid = comps[0]; + String identityKey = comps[1]; + String identityIndex = comps[2]; + if (!keyUuid.equals(accountUuid) || !identityIndex.equals(identity)) { + // Skip entries that belong to another identity + continue; + } + + SettingsDescription setting = IdentitySettings.SETTINGS.get(identityKey); + if (setting != null) { + // Only write settings that have an entry in IdentitySettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, identityKey, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Identity setting \"" + identityKey + + "\" has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + serializer.endTag(null, SETTINGS_ELEMENT); + + serializer.endTag(null, IDENTITY_ELEMENT); + } + + private static void writeFolder(XmlSerializer serializer, String accountUuid, + String folder, Map prefs) throws IOException { + + serializer.startTag(null, FOLDER_ELEMENT); + serializer.attribute(null, NAME_ATTRIBUTE, folder); + + // Write folder settings + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 3) { + // Skip non-folder config entries + continue; + } + + String keyUuid = comps[0]; + String folderName = comps[1]; + String folderKey = comps[2]; + + if (!keyUuid.equals(accountUuid) || !folderName.equals(folder)) { + // Skip entries that belong to another folder + continue; + } + + SettingsDescription setting = FolderSettings.SETTINGS.get(folderKey); + if (setting != null) { + // Only write settings that have an entry in FolderSettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, folderKey, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Folder setting \"" + folderKey + + "\" has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + + serializer.endTag(null, FOLDER_ELEMENT); + } + + private static void writeElement(XmlSerializer serializer, String elementName, String value) + throws IllegalArgumentException, IllegalStateException, IOException { + if (value != null) { + serializer.startTag(null, elementName); + serializer.text(value); + serializer.endTag(null, elementName); + } + } + + private static void writeKeyValue(XmlSerializer serializer, String key, String value) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(null, VALUE_ELEMENT); + serializer.attribute(null, KEY_ATTRIBUTE, key); + if (value != null) { + serializer.text(value); + } + serializer.endTag(null, VALUE_ELEMENT); + } +} diff --git a/src/com/fsck/k9/preferences/SettingsImportExportException.java b/src/com/fsck/k9/preferences/SettingsImportExportException.java new file mode 100644 index 000000000..616a466bf --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsImportExportException.java @@ -0,0 +1,21 @@ +package com.fsck.k9.preferences; + +public class SettingsImportExportException extends Exception { + + public SettingsImportExportException() { + super(); + } + + public SettingsImportExportException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public SettingsImportExportException(String detailMessage) { + super(detailMessage); + } + + public SettingsImportExportException(Throwable throwable) { + super(throwable); + } + +} diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java new file mode 100644 index 000000000..4920a3acc --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsImporter.java @@ -0,0 +1,1109 @@ +package com.fsck.k9.preferences; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.fsck.k9.Account; +import com.fsck.k9.Identity; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.helper.DateFormatter; +import com.fsck.k9.helper.Utility; +import com.fsck.k9.mail.ConnectionSecurity; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.store.WebDavStore; +import com.fsck.k9.preferences.Settings.InvalidSettingValueException; + +public class SettingsImporter { + + /** + * Class to list the contents of an import file/stream. + * + * @see SettingsImporter#getImportStreamContents(InputStream) + */ + public static class ImportContents { + /** + * True, if the import file contains global settings. + */ + public final boolean globalSettings; + + /** + * The list of accounts found in the import file. Never {@code null}. + */ + public final List accounts; + + private ImportContents(boolean globalSettings, List accounts) { + this.globalSettings = globalSettings; + this.accounts = accounts; + } + } + + /** + * Class to describe an account (name, UUID). + * + * @see ImportContents + */ + public static class AccountDescription { + /** + * The name of the account. + */ + public final String name; + + /** + * The UUID of the account. + */ + public final String uuid; + + private AccountDescription(String name, String uuid) { + this.name = name; + this.uuid = uuid; + } + } + + public static class AccountDescriptionPair { + public final AccountDescription original; + public final AccountDescription imported; + public final boolean overwritten; + + private AccountDescriptionPair(AccountDescription original, AccountDescription imported, + boolean overwritten) { + this.original = original; + this.imported = imported; + this.overwritten = overwritten; + } + } + + public static class ImportResults { + public final boolean globalSettings; + public final List importedAccounts; + public final List errorneousAccounts; + + private ImportResults(boolean globalSettings, + List importedAccounts, + List errorneousAccounts) { + this.globalSettings = globalSettings; + this.importedAccounts = importedAccounts; + this.errorneousAccounts = errorneousAccounts; + } + } + + /** + * Parses an import {@link InputStream} and returns information on whether it contains global + * settings and/or account settings. For all account configurations found, the name of the + * account along with the account UUID is returned. + * + * @param inputStream + * An {@code InputStream} to read the settings from. + * + * @return An {@link ImportContents} instance containing information about the contents of the + * settings file. + * + * @throws SettingsImportExportException + * In case of an error. + */ + public static ImportContents getImportStreamContents(InputStream inputStream) + throws SettingsImportExportException { + + try { + // Parse the import stream but don't save individual settings (overview=true) + Imported imported = parseSettings(inputStream, false, null, true); + + // If the stream contains global settings the "globalSettings" member will not be null + boolean globalSettings = (imported.globalSettings != null); + + final List accounts = new ArrayList(); + // If the stream contains at least one account configuration the "accounts" member + // will not be null. + if (imported.accounts != null) { + for (ImportedAccount account : imported.accounts.values()) { + accounts.add(new AccountDescription(account.name, account.uuid)); + } + } + + //TODO: throw exception if neither global settings nor account settings could be found + + return new ImportContents(globalSettings, accounts); + + } catch (SettingsImportExportException e) { + throw e; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + /** + * Reads an import {@link InputStream} and imports the global settings and/or account + * configurations specified by the arguments. + * + * @param context + * A {@link Context} instance. + * @param inputStream + * The {@code InputStream} to read the settings from. + * @param globalSettings + * {@code true} if global settings should be imported from the file. + * @param accountUuids + * A list of UUIDs of the accounts that should be imported. + * @param overwrite + * {@code true} if existing accounts should be overwritten when an account with the + * same UUID is found in the settings file.
+ * Note: This can have side-effects we currently don't handle, e.g. + * changing the account type from IMAP to POP3. So don't use this for now! + * + * @return An {@link ImportResults} instance containing information about errors and + * successfully imported accounts. + * + * @throws SettingsImportExportException + * In case of an error. + */ + public static ImportResults importSettings(Context context, InputStream inputStream, + boolean globalSettings, List accountUuids, boolean overwrite) + throws SettingsImportExportException { + + try + { + boolean globalSettingsImported = false; + List importedAccounts = new ArrayList(); + List errorneousAccounts = new ArrayList(); + + Imported imported = parseSettings(inputStream, globalSettings, accountUuids, false); + + Preferences preferences = Preferences.getPreferences(context); + SharedPreferences storage = preferences.getPreferences(); + + if (globalSettings) { + try { + SharedPreferences.Editor editor = storage.edit(); + if (imported.globalSettings != null) { + importGlobalSettings(storage, editor, imported.globalSettings); + } else { + Log.w(K9.LOG_TAG, "Was asked to import global settings but none found."); + } + if (editor.commit()) { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Committed global settings to the preference " + + "storage."); + } + globalSettingsImported = true; + } else { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Failed to commit global settings to the " + + "preference storage"); + } + } + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Exception while importing global settings", e); + } + } + + if (accountUuids != null && accountUuids.size() > 0) { + if (imported.accounts != null) { + List newUuids = new ArrayList(); + for (String accountUuid : accountUuids) { + if (imported.accounts.containsKey(accountUuid)) { + ImportedAccount account = imported.accounts.get(accountUuid); + try { + SharedPreferences.Editor editor = storage.edit(); + + AccountDescriptionPair importResult = importAccount(context, + editor, account, overwrite); + + String newUuid = importResult.imported.uuid; + if (!importResult.overwritten) { + newUuids.add(newUuid); + } + if (editor.commit()) { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Committed settings for account \"" + + importResult.imported.name + + "\" to the settings database."); + } + importedAccounts.add(importResult); + } else { + if (K9.DEBUG) { + Log.w(K9.LOG_TAG, "Error while committing settings for " + + "account \"" + importResult.original.name + + "\" to the settings database."); + } + errorneousAccounts.add(importResult.original); + } + } catch (InvalidSettingValueException e) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Encountered invalid setting while " + + "importing account \"" + account.name + "\"", e); + } + errorneousAccounts.add(new AccountDescription(account.name, account.uuid)); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Exception while importing account \"" + + account.name + "\"", e); + errorneousAccounts.add(new AccountDescription(account.name, account.uuid)); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import account with UUID " + + accountUuid + ". But this account wasn't found."); + } + } + + SharedPreferences.Editor editor = storage.edit(); + + if (newUuids.size() > 0) { + String oldAccountUuids = storage.getString("accountUuids", ""); + String appendUuids = Utility.combine(newUuids.toArray(new String[0]), ','); + String prefix = ""; + if (oldAccountUuids.length() > 0) { + prefix = oldAccountUuids + ","; + } + putString(editor, "accountUuids", prefix + appendUuids); + } + + String defaultAccountUuid = storage.getString("defaultAccountUuid", null); + if (defaultAccountUuid == null) { + putString(editor, "defaultAccountUuid", accountUuids.get(0)); + } + + if (!editor.commit()) { + throw new SettingsImportExportException("Failed to set default account"); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import at least one account but none found."); + } + } + + preferences.loadAccounts(); + DateFormatter.clearChosenFormat(); + K9.loadPrefs(preferences); + K9.setServicesEnabled(context); + + return new ImportResults(globalSettingsImported, importedAccounts, errorneousAccounts); + + } catch (SettingsImportExportException e) { + throw e; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + private static void importGlobalSettings(SharedPreferences storage, + SharedPreferences.Editor editor, ImportedSettings settings) { + + Map validatedSettings = GlobalSettings.validate(settings.settings); + + // Use current global settings as base and overwrite with validated settings read from the + // import file. + Map mergedSettings = + new HashMap(GlobalSettings.getGlobalSettings(storage)); + mergedSettings.putAll(validatedSettings); + + for (Map.Entry setting : mergedSettings.entrySet()) { + String key = setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + } + + private static AccountDescriptionPair importAccount(Context context, + SharedPreferences.Editor editor, ImportedAccount account, boolean overwrite) + throws InvalidSettingValueException { + + AccountDescription original = new AccountDescription(account.name, account.uuid); + + Preferences prefs = Preferences.getPreferences(context); + Account[] accounts = prefs.getAccounts(); + + String uuid = account.uuid; + Account existingAccount = prefs.getAccount(uuid); + boolean mergeImportedAccount = (overwrite && existingAccount != null); + + if (!overwrite && existingAccount != null) { + // An account with this UUID already exists, but we're not allowed to overwrite it. + // So generate a new UUID. + uuid = UUID.randomUUID().toString(); + } + + // Make sure the account name is unique + String accountName = (account.name != null) ? account.name : "Imported"; + if (isAccountNameUsed(accountName, accounts)) { + // Account name is already in use. So generate a new one by appending " (x)", where x + // is the first number >= 1 that results in an unused account name. + for (int i = 1; i <= accounts.length; i++) { + accountName = account.name + " (" + i + ")"; + if (!isAccountNameUsed(accountName, accounts)) { + break; + } + } + } + + // Write account name + String accountKeyPrefix = uuid + "."; + putString(editor, accountKeyPrefix + Account.ACCOUNT_DESCRIPTION_KEY, accountName); + + if (account.incoming == null) { + // We don't import accounts without incoming server settings + throw new InvalidSettingValueException(); + } + + // Write incoming server settings (storeUri) + ServerSettings incoming = new ImportedServerSettings(account.incoming); + String storeUri = Store.createStoreUri(incoming); + putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri)); + + // Mark account as disabled if the settings file didn't contain a password + boolean createAccountDisabled = (incoming.password == null || + incoming.password.length() == 0); + + if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) { + // All account types except WebDAV need to provide outgoing server settings + throw new InvalidSettingValueException(); + } + + if (account.outgoing != null) { + // Write outgoing server settings (transportUri) + ServerSettings outgoing = new ImportedServerSettings(account.outgoing); + String transportUri = Transport.createTransportUri(outgoing); + putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri)); + + // Mark account as disabled if the settings file didn't contain a password + if (outgoing.password == null || outgoing.password.length() == 0) { + createAccountDisabled = true; + } + } + + // Write key to mark account as disabled if necessary + if (createAccountDisabled) { + editor.putBoolean(accountKeyPrefix + "enabled", false); + } + + // Validate account settings + Map validatedSettings = + AccountSettings.validate(account.settings.settings, !mergeImportedAccount); + + // Merge account settings if necessary + Map writeSettings; + if (mergeImportedAccount) { + writeSettings = new HashMap( + AccountSettings.getAccountSettings(prefs.getPreferences(), uuid)); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write account settings + for (Map.Entry setting : writeSettings.entrySet()) { + String key = accountKeyPrefix + setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + + // If it's a new account generate and write a new "accountNumber" + if (!mergeImportedAccount) { + int newAccountNumber = Account.generateAccountNumber(prefs); + putString(editor, accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber)); + } + + // Write identities + if (account.identities != null) { + importIdentities(editor, uuid, account, overwrite, existingAccount, prefs); + } else if (!mergeImportedAccount) { + // Require accounts to at least have one identity + throw new InvalidSettingValueException(); + } + + // Write folder settings + if (account.folders != null) { + for (ImportedFolder folder : account.folders) { + importFolder(editor, uuid, folder, mergeImportedAccount, prefs); + } + } + + //TODO: sync folder settings with localstore? + + AccountDescription imported = new AccountDescription(accountName, uuid); + return new AccountDescriptionPair(original, imported, mergeImportedAccount); + } + + private static void importFolder(SharedPreferences.Editor editor, String uuid, + ImportedFolder folder, boolean overwrite, Preferences prefs) { + + // Validate folder settings + Map validatedSettings = + FolderSettings.validate(folder.settings.settings, !overwrite); + + // Merge folder settings if necessary + Map writeSettings; + if (overwrite) { + writeSettings = FolderSettings.getFolderSettings(prefs.getPreferences(), + uuid, folder.name); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write folder settings + String prefix = uuid + "." + folder.name + "."; + for (Map.Entry setting : writeSettings.entrySet()) { + String key = prefix + setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + } + + private static void importIdentities(SharedPreferences.Editor editor, String uuid, + ImportedAccount account, boolean overwrite, Account existingAccount, + Preferences prefs) throws InvalidSettingValueException { + + String accountKeyPrefix = uuid + "."; + + // Gather information about existing identities for this account (if any) + int nextIdentityIndex = 0; + final List existingIdentities; + if (overwrite && existingAccount != null) { + existingIdentities = existingAccount.getIdentities(); + nextIdentityIndex = existingIdentities.size(); + } else { + existingIdentities = new ArrayList(); + } + + // Write identities + for (ImportedIdentity identity : account.identities) { + int writeIdentityIndex = nextIdentityIndex; + boolean mergeSettings = false; + if (overwrite && existingIdentities.size() > 0) { + int identityIndex = findIdentity(identity, existingIdentities); + if (identityIndex != -1) { + writeIdentityIndex = identityIndex; + mergeSettings = true; + } + } + if (!mergeSettings) { + nextIdentityIndex++; + } + + String identityDescription = (identity.description == null) ? + "Imported" : identity.description; + if (isIdentityDescriptionUsed(identityDescription, existingIdentities)) { + // Identity description is already in use. So generate a new one by appending + // " (x)", where x is the first number >= 1 that results in an unused identity + // description. + for (int i = 1; i <= existingIdentities.size(); i++) { + identityDescription = identity.description + " (" + i + ")"; + if (!isIdentityDescriptionUsed(identityDescription, existingIdentities)) { + break; + } + } + } + + String identitySuffix = "." + writeIdentityIndex; + + // Write name used in identity + String identityName = (identity.name == null) ? "" : identity.name; + putString(editor, accountKeyPrefix + Account.IDENTITY_NAME_KEY + identitySuffix, + identityName); + + // Validate email address + if (!IdentitySettings.isEmailAddressValid(identity.email)) { + throw new InvalidSettingValueException(); + } + + // Write email address + putString(editor, accountKeyPrefix + Account.IDENTITY_EMAIL_KEY + identitySuffix, + identity.email); + + // Write identity description + putString(editor, accountKeyPrefix + Account.IDENTITY_DESCRIPTION_KEY + identitySuffix, + identityDescription); + + if (identity.settings != null) { + // Validate identity settings + Map validatedSettings = IdentitySettings.validate( + identity.settings.settings, !mergeSettings); + + // Merge identity settings if necessary + Map writeSettings; + if (mergeSettings) { + writeSettings = new HashMap(IdentitySettings.getIdentitySettings( + prefs.getPreferences(), uuid, writeIdentityIndex)); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write identity settings + for (Map.Entry setting : writeSettings.entrySet()) { + String key = accountKeyPrefix + setting.getKey() + identitySuffix; + String value = setting.getValue(); + putString(editor, key, value); + } + } + } + } + + private static boolean isAccountNameUsed(String name, Account[] accounts) { + for (Account account : accounts) { + if (account.getDescription().equals(name)) { + return true; + } + } + return false; + } + + private static boolean isIdentityDescriptionUsed(String description, List identities) { + for (Identity identitiy : identities) { + if (identitiy.getDescription().equals(description)) { + return true; + } + } + return false; + } + + private static int findIdentity(ImportedIdentity identity, + List identities) { + for (int i = 0; i < identities.size(); i++) { + Identity existingIdentity = identities.get(i); + if (existingIdentity.getName().equals(identity.name) && + existingIdentity.getEmail().equals(identity.email)) { + return i; + } + } + return -1; + } + + /** + * Write to an {@link SharedPreferences.Editor} while logging what is written if debug logging + * is enabled. + * + * @param editor + * The {@code Editor} to write to. + * @param key + * The name of the preference to modify. + * @param value + * The new value for the preference. + */ + private static void putString(SharedPreferences.Editor editor, String key, String value) { + if (K9.DEBUG) { + String outputValue = value; + if (!K9.DEBUG_SENSITIVE && + (key.endsWith(".transportUri") || key.endsWith(".storeUri"))) { + outputValue = "*sensitive*"; + } + Log.v(K9.LOG_TAG, "Setting " + key + "=" + outputValue); + } + editor.putString(key, value); + } + + private static Imported parseSettings(InputStream inputStream, boolean globalSettings, + List accountUuids, boolean overview) + throws SettingsImportExportException { + + if (!overview && accountUuids == null) { + throw new IllegalArgumentException("Argument 'accountUuids' must not be null."); + } + + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + //factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + + InputStreamReader reader = new InputStreamReader(inputStream); + xpp.setInput(reader); + + Imported imported = null; + int eventType = xpp.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if(eventType == XmlPullParser.START_TAG) { + if (SettingsExporter.ROOT_ELEMENT.equals(xpp.getName())) { + imported = parseRoot(xpp, globalSettings, accountUuids, overview); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + if (imported == null || (overview && imported.globalSettings == null && + imported.accounts == null)) { + throw new SettingsImportExportException("Invalid import data"); + } + + return imported; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + private static void skipToEndTag(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + eventType = xpp.next(); + } + } + + private static String getText(XmlPullParser xpp) + throws XmlPullParserException, IOException { + + int eventType = xpp.next(); + if (eventType != XmlPullParser.TEXT) { + return ""; + } + return xpp.getText(); + } + + private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings, + List accountUuids, boolean overview) + throws XmlPullParserException, IOException, SettingsImportExportException { + + Imported result = new Imported(); + + String fileFormatVersionString = xpp.getAttributeValue(null, + SettingsExporter.FILE_FORMAT_ATTRIBUTE); + validateFileFormatVersion(fileFormatVersionString); + + String contentVersionString = xpp.getAttributeValue(null, + SettingsExporter.VERSION_ATTRIBUTE); + validateContentVersion(contentVersionString); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ROOT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.GLOBAL_ELEMENT.equals(element)) { + if (overview || globalSettings) { + if (result.globalSettings == null) { + if (overview) { + result.globalSettings = new ImportedSettings(); + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + } else { + result.globalSettings = parseSettings(xpp, SettingsExporter.GLOBAL_ELEMENT); + } + } else { + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + Log.w(K9.LOG_TAG, "More than one global settings element. Only using the first one!"); + } + } else { + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping global settings"); + } + } else if (SettingsExporter.ACCOUNTS_ELEMENT.equals(element)) { + if (result.accounts == null) { + result.accounts = parseAccounts(xpp, accountUuids, overview); + } else { + Log.w(K9.LOG_TAG, "More than one accounts element. Only using the first one!"); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return result; + } + + private static int validateFileFormatVersion(String versionString) + throws SettingsImportExportException { + + if (versionString == null) { + throw new SettingsImportExportException("Missing file format version"); + } + + int version; + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException e) { + throw new SettingsImportExportException("Invalid file format version: " + + versionString); + } + + if (version != SettingsExporter.FILE_FORMAT_VERSION) { + throw new SettingsImportExportException("Unsupported file format version: " + + versionString); + } + + return version; + } + + private static int validateContentVersion(String versionString) + throws SettingsImportExportException { + + if (versionString == null) { + throw new SettingsImportExportException("Missing content version"); + } + + int version; + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException e) { + throw new SettingsImportExportException("Invalid content version: " + + versionString); + } + + if (version != Settings.VERSION) { + throw new SettingsImportExportException("Unsupported content version: " + + versionString); + } + + return version; + } + + private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + + ImportedSettings result = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.VALUE_ELEMENT.equals(element)) { + String key = xpp.getAttributeValue(null, SettingsExporter.KEY_ATTRIBUTE); + String value = getText(xpp); + + if (result == null) { + result = new ImportedSettings(); + } + + if (result.settings.containsKey(key)) { + Log.w(K9.LOG_TAG, "Already read key \"" + key + "\". Ignoring value \"" + value + "\""); + } else { + result.settings.put(key, value); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return result; + } + + private static Map parseAccounts(XmlPullParser xpp, + List accountUuids, boolean overview) + throws XmlPullParserException, IOException { + + Map accounts = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ACCOUNTS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.ACCOUNT_ELEMENT.equals(element)) { + if (accounts == null) { + accounts = new HashMap(); + } + + ImportedAccount account = parseAccount(xpp, accountUuids, overview); + + if (account == null) { + // Do nothing - parseAccount() already logged a message + } else if (!accounts.containsKey(account.uuid)) { + accounts.put(account.uuid, account); + } else { + Log.w(K9.LOG_TAG, "Duplicate account entries with UUID " + account.uuid + + ". Ignoring!"); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return accounts; + } + + private static ImportedAccount parseAccount(XmlPullParser xpp, List accountUuids, + boolean overview) + throws XmlPullParserException, IOException { + + String uuid = xpp.getAttributeValue(null, SettingsExporter.UUID_ATTRIBUTE); + + try { + UUID.fromString(uuid); + } catch (Exception e) { + skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT); + Log.w(K9.LOG_TAG, "Skipping account with invalid UUID " + uuid); + return null; + } + + ImportedAccount account = new ImportedAccount(); + account.uuid = uuid; + + if (overview || accountUuids.contains(uuid)) { + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ACCOUNT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.NAME_ELEMENT.equals(element)) { + account.name = getText(xpp); + } else if (SettingsExporter.INCOMING_SERVER_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT); + } else { + account.incoming = parseServerSettings(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT); + } + } else if (SettingsExporter.OUTGOING_SERVER_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT); + } else { + account.outgoing = parseServerSettings(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT); + } + } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.SETTINGS_ELEMENT); + } else { + account.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT); + } + } else if (SettingsExporter.IDENTITIES_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.IDENTITIES_ELEMENT); + } else { + account.identities = parseIdentities(xpp); + } + } else if (SettingsExporter.FOLDERS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.FOLDERS_ELEMENT); + } else { + account.folders = parseFolders(xpp); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + } else { + skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping account with UUID " + uuid); + } + + return account; + } + + private static ImportedServer parseServerSettings(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + ImportedServer server = new ImportedServer(); + + server.type = xpp.getAttributeValue(null, SettingsExporter.TYPE_ATTRIBUTE); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.HOST_ELEMENT.equals(element)) { + server.host = getText(xpp); + } else if (SettingsExporter.PORT_ELEMENT.equals(element)) { + server.port = getText(xpp); + } else if (SettingsExporter.CONNECTION_SECURITY_ELEMENT.equals(element)) { + server.connectionSecurity = getText(xpp); + } else if (SettingsExporter.AUTHENTICATION_TYPE_ELEMENT.equals(element)) { + server.authenticationType = getText(xpp); + } else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) { + server.username = getText(xpp); + } else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) { + server.password = getText(xpp); + } else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) { + server.extras = parseSettings(xpp, SettingsExporter.EXTRA_ELEMENT); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return server; + } + + private static List parseIdentities(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List identities = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.IDENTITIES_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.IDENTITY_ELEMENT.equals(element)) { + if (identities == null) { + identities = new ArrayList(); + } + + ImportedIdentity identity = parseIdentity(xpp); + identities.add(identity); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return identities; + } + + private static ImportedIdentity parseIdentity(XmlPullParser xpp) + throws XmlPullParserException, IOException { + ImportedIdentity identity = new ImportedIdentity(); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.IDENTITY_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.NAME_ELEMENT.equals(element)) { + identity.name = getText(xpp); + } else if (SettingsExporter.EMAIL_ELEMENT.equals(element)) { + identity.email = getText(xpp); + } else if (SettingsExporter.DESCRIPTION_ELEMENT.equals(element)) { + identity.description = getText(xpp); + } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) { + identity.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return identity; + } + + private static List parseFolders(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List folders = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.FOLDERS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.FOLDER_ELEMENT.equals(element)) { + if (folders == null) { + folders = new ArrayList(); + } + + ImportedFolder folder = parseFolder(xpp); + folders.add(folder); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return folders; + } + + private static ImportedFolder parseFolder(XmlPullParser xpp) + throws XmlPullParserException, IOException { + ImportedFolder folder = new ImportedFolder(); + + String name = xpp.getAttributeValue(null, SettingsExporter.NAME_ATTRIBUTE); + folder.name = name; + + folder.settings = parseSettings(xpp, SettingsExporter.FOLDER_ELEMENT); + + return folder; + } + + private static class ImportedServerSettings extends ServerSettings { + private final ImportedServer mImportedServer; + + public ImportedServerSettings(ImportedServer server) { + super(server.type, server.host, convertPort(server.port), + convertConnectionSecurity(server.connectionSecurity), + server.authenticationType, server.username, server.password); + mImportedServer = server; + } + + @Override + public Map getExtra() { + return (mImportedServer.extras != null) ? + Collections.unmodifiableMap(mImportedServer.extras.settings) : null; + } + + private static int convertPort(String port) { + try { + return Integer.parseInt(port); + } catch (NumberFormatException e) { + return -1; + } + } + + private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) { + try { + return ConnectionSecurity.valueOf(connectionSecurity); + } catch (Exception e) { + return ConnectionSecurity.NONE; + } + } + } + + private static class Imported { + public ImportedSettings globalSettings; + public Map accounts; + } + + private static class ImportedSettings { + public Map settings = new HashMap(); + } + + private static class ImportedAccount { + public String uuid; + public String name; + public ImportedServer incoming; + public ImportedServer outgoing; + public ImportedSettings settings; + public List identities; + public List folders; + } + + private static class ImportedServer { + public String type; + public String host; + public String port; + public String connectionSecurity; + public String authenticationType; + public String username; + public String password; + public ImportedSettings extras; + } + + private static class ImportedIdentity { + public String name; + public String email; + public String description; + public ImportedSettings settings; + } + + private static class ImportedFolder { + public String name; + public ImportedSettings settings; + } +} diff --git a/src/com/fsck/k9/preferences/TimePickerPreference.java b/src/com/fsck/k9/preferences/TimePickerPreference.java index f829cebfc..a2d6592b3 100644 --- a/src/com/fsck/k9/preferences/TimePickerPreference.java +++ b/src/com/fsck/k9/preferences/TimePickerPreference.java @@ -20,7 +20,7 @@ public class TimePickerPreference extends DialogPreference implements /** * The validation expression for this preference */ - private static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]"; + public static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]"; /** * The default value for this preference diff --git a/src/com/fsck/k9/service/MailService.java b/src/com/fsck/k9/service/MailService.java index 55a668ef1..8f3330073 100644 --- a/src/com/fsck/k9/service/MailService.java +++ b/src/com/fsck/k9/service/MailService.java @@ -274,7 +274,7 @@ public class MailService extends CoreService { } int shortestInterval = -1; - for (Account account : prefs.getAccounts()) { + for (Account account : prefs.getAvailableAccounts()) { if (account.getAutomaticCheckIntervalMinutes() != -1 && account.getFolderSyncMode() != FolderMode.NONE && (account.getAutomaticCheckIntervalMinutes() < shortestInterval || @@ -363,7 +363,7 @@ public class MailService extends CoreService { for (Account account : Preferences.getPreferences(MailService.this).getAccounts()) { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Setting up pushers for account " + account.getDescription()); - if (account.isAvailable(getApplicationContext())) { + if (account.isEnabled() && account.isAvailable(getApplicationContext())) { pushing |= MessagingController.getInstance(getApplication()).setupPushing(account); } else { //TODO: setupPushing of unavailable accounts when they become available (sd-card inserted) diff --git a/src/com/fsck/k9/view/AccessibleWebView.java b/src/com/fsck/k9/view/AccessibleWebView.java index 7112e6dab..5dd8bb9a8 100644 --- a/src/com/fsck/k9/view/AccessibleWebView.java +++ b/src/com/fsck/k9/view/AccessibleWebView.java @@ -26,11 +26,15 @@ import android.webkit.WebView; import android.widget.TextView; import com.fsck.k9.activity.AccessibleEmailContentActivity; +import com.fsck.k9.controller.MessagingListener; + +import java.util.Set; public class AccessibleWebView extends TextView { private Context mContext; private String mHtmlSource; private WebView mDummyWebView; + private Set mListeners = null; public AccessibleWebView(Context context) { super(context); @@ -68,6 +72,13 @@ public class AccessibleWebView extends TextView { String historyUrl) { mHtmlSource = data; this.setText(Html.fromHtml(mHtmlSource, null, null)); + + // Let everyone know that loading has finished. + if (mListeners != null) { + for (MessagingListener l : mListeners) { + l.messageViewFinished(); + } + } } public boolean zoomIn() { @@ -92,4 +103,8 @@ public class AccessibleWebView extends TextView { i.putExtra("content", mHtmlSource); mContext.startActivity(i); } + + public void setListeners(final Set listeners) { + this.mListeners = listeners; + } } diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index cf24aa322..9e9a4c441 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -239,8 +239,10 @@ public class AttachmentView extends FrameLayout { public void showFile() { Uri uri = AttachmentProvider.getAttachmentUriForViewing(mAccount, part.getAttachmentId()); Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(uri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // We explicitly set the ContentType in addition to the URI because some attachment viewers (such as Polaris office 3.0.x) choke on documents without a mime type + intent.setDataAndType(uri, contentType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + try { mContext.startActivity(intent); } catch (Exception e) { @@ -271,7 +273,7 @@ public class AttachmentView extends FrameLayout { Uri uri = AttachmentProvider.getAttachmentUriForViewing(mAccount, part.getAttachmentId()); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); if (intent.resolveActivity(mContext.getPackageManager()) == null) { viewButton.setEnabled(false); } diff --git a/src/com/fsck/k9/view/MessageWebView.java b/src/com/fsck/k9/view/MessageWebView.java index 9871e503e..4d22c07d6 100644 --- a/src/com/fsck/k9/view/MessageWebView.java +++ b/src/com/fsck/k9/view/MessageWebView.java @@ -1,6 +1,7 @@ package com.fsck.k9.view; import android.content.Context; +import android.graphics.Picture; import android.os.Build; import android.util.AttributeSet; import android.util.Log; @@ -10,10 +11,15 @@ import android.webkit.WebView; import android.widget.Toast; import com.fsck.k9.K9; import com.fsck.k9.R; +import com.fsck.k9.controller.MessagingListener; import java.lang.reflect.Method; +import java.util.Set; public class MessageWebView extends WebView { + // Store a reference to the listeners in MessagingController. We can't fetch it directly since + // we don't know the application name. + private Set mListeners = null; /** * We use WebSettings.getBlockNetworkLoads() to prevent the WebView that displays email @@ -97,6 +103,18 @@ public class MessageWebView extends WebView { // Disable network images by default. This is overridden by preferences. blockNetworkData(true); + + // Listen for when the screen has finished drawing. + setPictureListener(new PictureListener() { + @Override + public void onNewPicture(WebView webView, Picture picture) { + if (mListeners != null) { + for (MessagingListener l : mListeners) { + l.messageViewFinished(); + } + } + } + }); } /* @@ -115,4 +133,8 @@ public class MessageWebView extends WebView { Log.e(K9.LOG_TAG, "Exception in emulateShiftHeld()", e); } } + + public void setListeners(final Set listeners) { + this.mListeners = listeners; + } } diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index 85afd51f3..48206f8ed 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -29,6 +29,7 @@ import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.LocalStore; import java.util.List; +import java.util.Set; /** @@ -116,11 +117,10 @@ public class SingleMessageView extends LinearLayout { return false; } - - public boolean showPictures() { return mShowPictures; } + public void setShowPictures(Boolean show) { mShowPictures = show; } @@ -312,4 +312,21 @@ public class SingleMessageView extends LinearLayout { AttachmentView.AttachmentFileDownloadCallback attachmentCallback) { this.attachmentCallback = attachmentCallback; } + + /** + * Save a copy of the {@link com.fsck.k9.controller.MessagingController#getListeners()}. This method will also + * pass along these listeners to the underlying views. + * @param listeners Set of listeners. + */ + public void setListeners(final Set listeners) { + if(!mScreenReaderEnabled) { + if(mMessageContentView != null) { + mMessageContentView.setListeners(listeners); + } + } else { + if(mAccessibleMessageContentView != null) { + mAccessibleMessageContentView.setListeners(listeners); + } + } + } } diff --git a/src/com/fsck/k9/view/ToggleScrollView.java b/src/com/fsck/k9/view/ToggleScrollView.java index 16ee41ab1..714e8ce24 100644 --- a/src/com/fsck/k9/view/ToggleScrollView.java +++ b/src/com/fsck/k9/view/ToggleScrollView.java @@ -2,13 +2,22 @@ package com.fsck.k9.view; import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.ScrollView; +import com.fsck.k9.K9; +import com.fsck.k9.controller.MessagingListener; +/** + * An extension of {@link ScrollView} that allows scrolling to be selectively disabled. + */ public class ToggleScrollView extends ScrollView { private GestureDetector mDetector; private boolean mScrolling = true; + private int mCurrentYPosition; + private double mScrollPercentage; + private ScrollToLastLocationListener mListener; public ToggleScrollView(Context context, AttributeSet attrs) { super(context, attrs); @@ -59,4 +68,76 @@ public class ToggleScrollView extends ScrollView { return false; } } + + /** + * Fetch the current percentage by which this view has been scrolled. + * @return Scroll percentage based on the top edge of the screen, from 0 to 100. This number should never really + * be 100, unless the screen is of 0 height... + */ + public double getScrollPercentage() { + // We save only the Y coordinate instead of the percentage because I don't know how expensive the + // computeVerticalScrollRange() call is. + final int scrollRange = computeVerticalScrollRange(); + if(scrollRange == 0) { + return 0; + } + return (double) mCurrentYPosition / scrollRange; + } + + /** + * Set the percentage by which we should scroll the page once we get the load complete event. This is + * based on the top edge of the view. + * @param percentage Percentage of page to scroll to. + */ + public void setScrollPercentage(final double percentage) { + Log.d(K9.LOG_TAG, "ToggleView: Setting last scroll percentage to " + percentage); + this.mScrollPercentage = percentage; + } + + /** + * Override {@link ScrollView#onScrollChanged(int, int, int, int)} to record the current x/y position. We use this + * to save our current position for future scrolling. + * + * @param x + * @param y + * @param oldx + * @param oldy + */ + @Override + protected void onScrollChanged(int x, int y, int oldx, int oldy) { + super.onScrollChanged(x, y, oldx, oldy); + + this.mCurrentYPosition = y; + // I wish Android has a TRACE log level so I wouldn't have to comment this out. This one is really noisy. + // Log.d(K9.LOG_TAG, "ToggleScrollView: mCurrentYPosition=" + y + " scrollRange=" + computeVerticalScrollRange() + " pct=" + getScrollPercentage()); + } + + /** + * This is a {@link MessagingListener} which listens for when the a message has finished being displayed on the + * screen. We'll scroll the message to the user's last known location once it's done. + */ + class ScrollToLastLocationListener extends MessagingListener { + public void messageViewFinished() { + // Don't scroll if our last position was at the top. + if(mScrollPercentage != 0.0) { + final int scrollRange = computeVerticalScrollRange(); + final int newY = (int)(mScrollPercentage * scrollRange); + Log.d(K9.LOG_TAG, "ToggleScrollView: requested " + (100 * mScrollPercentage) + "%, " + + "scrolling to " + newY + "/" + scrollRange); + scrollTo(0, newY); + } + } + } + + /** + * Fetch the {@link MessagingListener} for this ScrollView. + * @return + */ + public MessagingListener getListener() { + if(this.mListener != null) { + return this.mListener; + } else { + return this.mListener = new ScrollToLastLocationListener(); + } + } }